Text Editor with RTF support

Asked

Viewed 730 times

3

I need to find a text editor with RTF support.

Goal: Write and format in html and save in RTF, then take RTF and edit in html.

I searched the documentation of several publishers to see if there is something of the type and I could not find.

I need in RTF why the application made in Delphi uses a text editor (TRichEdit) which is saved only in RTF, then my web application must understand RTF and be able to save so that reading in the two applications is done correctly.

Someone had the same problem?

What are the possible solutions to this ?

  • 1

    I do not understand what bootstrap has to do with rtf or Delphi, could explain better?

  • @Guilhermenascimento My html editor uses bootstrap called summernote, it infezlimente has no support for rtftohtml htmltorft conversion.

2 answers

3


To avoid future problems, I suggest you implement the two platforms (Web and Desktop) you can convert all RTF to HTML with the following procedure:

Include in your project:

  /**
   * RTF parser/formatter
   *
   * This code reads RTF files and formats the RTF data to HTML.
   *
   * PHP version 5
   *
   * @author     Alexander van Oostenrijk
   * @copyright  2014 Alexander van Oostenrijk
   * @license    GNU
   * @version    1
   * @link       http://www.independent-software.com
   * 
   * Sample of use:
   * 
   * $reader = new RtfReader();
   * $rtf = file_get_contents("itc.rtf"); // or use a string
   * $reader->Parse($rtf);
   * //$reader->root->dump(); // to see what the reader read
   * $formatter = new RtfHtml();
   * echo $formatter->Format($reader->root);   
   */

  class RtfElement
  {
    protected function Indent($level)
    {
      for($i = 0; $i < $level * 2; $i++) echo "&nbsp;";
    }
  }

  class RtfGroup extends RtfElement
  {
    public $parent;
    public $children;

    public function __construct()
    {
      $this->parent = null;
      $this->children = array();
    }

    public function GetType()
    {
      // No children?
      if(sizeof($this->children) == 0) return null;
      // First child not a control word?
      $child = $this->children[0];
      if(get_class($child) != "RtfControlWord") return null;
      return $child->word;
    }    

    public function IsDestination()
    {
      // No children?
      if(sizeof($this->children) == 0) return null;
      // First child not a control symbol?
      $child = $this->children[0];
      if(get_class($child) != "RtfControlSymbol") return null;
      return $child->symbol == '*';
    }

    public function dump($level = 0)
    {
      echo "<div>";
      $this->Indent($level);
      echo "{";
      echo "</div>";

      foreach($this->children as $child)
      {
        if(get_class($child) == "RtfGroup")
        {
          if ($child->GetType() == "fonttbl") continue;
          if ($child->GetType() == "colortbl") continue;
          if ($child->GetType() == "stylesheet") continue;
          if ($child->GetType() == "info") continue;
          // Skip any pictures:
          if (substr($child->GetType(), 0, 4) == "pict") continue;
          if ($child->IsDestination()) continue;
        }
        $child->dump($level + 2);
      }

      echo "<div>";
      $this->Indent($level);
      echo "}";
      echo "</div>";
    }
  }

  class RtfControlWord extends RtfElement
  {
    public $word;
    public $parameter;

    public function dump($level)
    {
      echo "<div style='color:green'>";
      $this->Indent($level);
      echo "WORD {$this->word} ({$this->parameter})";
      echo "</div>";
    }
  }

  class RtfControlSymbol extends RtfElement
  {
    public $symbol;
    public $parameter = 0;

    public function dump($level)
    {
      echo "<div style='color:blue'>";
      $this->Indent($level);
      echo "SYMBOL {$this->symbol} ({$this->parameter})";
      echo "</div>";
    }    
  }

  class RtfText extends RtfElement
  {
    public $text;

    public function dump($level)
    {
      echo "<div style='color:red'>";
      $this->Indent($level);
      echo "TEXT {$this->text}";
      echo "</div>";
    }    
  }

  class RtfReader
  {
    public $root = null;

    protected function GetChar()
    {
      $this->char = $this->rtf[$this->pos++];
    }

    protected function ParseStartGroup()
    {
      // Store state of document on stack.
      $group = new RtfGroup();
      if($this->group != null) $group->parent = $this->group;
      if($this->root == null)
      {
        $this->group = $group;
        $this->root = $group;
      }
      else
      {
        array_push($this->group->children, $group);
        $this->group = $group;
      }
    }

    protected function is_letter()
    {
      if(ord($this->char) >= 65 && ord($this->char) <= 90) return TRUE;
      if(ord($this->char) >= 90 && ord($this->char) <= 122) return TRUE;
      return FALSE;
    }

    protected function is_digit()
    {
      if(ord($this->char) >= 48 && ord($this->char) <= 57) return TRUE;
      return FALSE;
    }

    protected function ParseEndGroup()
    {
      // Retrieve state of document from stack.
      $this->group = $this->group->parent;
    }

    protected function ParseControlWord()
    {
      $this->GetChar();
      $word = "";
      while($this->is_letter())
      {
        $word .= $this->char;
        $this->GetChar();
      }

      // Read parameter (if any) consisting of digits.
      // Paramater may be negative.
      $parameter = null;
      $negative = false;
      if($this->char == '-') 
      {
        $this->GetChar();
        $negative = true;
      }
      while($this->is_digit())
      {
        if($parameter == null) $parameter = 0;
        $parameter = $parameter * 10 + $this->char;
        $this->GetChar();
      }
      if($parameter === null) $parameter = 1;
      if($negative) $parameter = -$parameter;

      // If this is \u, then the parameter will be followed by 
      // a character.
      if($word == "u") 
      {
      }
      // If the current character is a space, then
      // it is a delimiter. It is consumed.
      // If it's not a space, then it's part of the next
      // item in the text, so put the character back.
      else
      {
        if($this->char != ' ') $this->pos--; 
      }

      $rtfword = new RtfControlWord();
      $rtfword->word = $word;
      $rtfword->parameter = $parameter;
      array_push($this->group->children, $rtfword);
    }

    protected function ParseControlSymbol()
    {
      // Read symbol (one character only).
      $this->GetChar();
      $symbol = $this->char;

      // Symbols ordinarily have no parameter. However, 
      // if this is \', then it is followed by a 2-digit hex-code:
      $parameter = 0;
      if($symbol == '\'')
      {
        $this->GetChar(); 
        $parameter = $this->char;
        $this->GetChar(); 
        $parameter = hexdec($parameter . $this->char);
      }

      $rtfsymbol = new RtfControlSymbol();
      $rtfsymbol->symbol = $symbol;
      $rtfsymbol->parameter = $parameter;
      array_push($this->group->children, $rtfsymbol);
    }

    protected function ParseControl()
    {
      // Beginning of an RTF control word or control symbol.
      // Look ahead by one character to see if it starts with
      // a letter (control world) or another symbol (control symbol):
      $this->GetChar();
      $this->pos--;
      if($this->is_letter()) 
        $this->ParseControlWord();
      else
        $this->ParseControlSymbol();
    }

    protected function ParseText()
    {
      // Parse plain text up to backslash or brace,
      // unless escaped.
      $text = "";

      do
      {
        $terminate = false;
        $escape = false;

        // Is this an escape?
        if($this->char == '\\')
        {
          // Perform lookahead to see if this
          // is really an escape sequence.
          $this->GetChar();
          switch($this->char)
          {
            case '\\': $text .= '\\'; break;
            case '{': $text .= '{'; break;
            case '}': $text .= '}'; break;
            default:
              // Not an escape. Roll back.
              $this->pos = $this->pos - 2;
              $terminate = true;
              break;
          }
        }
        else if($this->char == '{' || $this->char == '}')
        {
          $this->pos--;
          $terminate = true;
        }

        if(!$terminate && !$escape)
        {
          $text .= $this->char;
          $this->GetChar();
        }
      }
      while(!$terminate && $this->pos < $this->len);

      $rtftext = new RtfText();
      $rtftext->text = $text;
      array_push($this->group->children, $rtftext);
    }

    public function Parse($rtf)
    {
      $this->rtf = $rtf;
      $this->pos = 0;
      $this->len = strlen($this->rtf);
      $this->group = null;
      $this->root = null;

      while($this->pos < $this->len)
      {
        // Read next character:
        $this->GetChar();

        // Ignore \r and \n
        if($this->char == "\n" || $this->char == "\r") continue;

        // What type of character is this?
        switch($this->char)
        {
          case '{':
            $this->ParseStartGroup();
            break;
          case '}':
            $this->ParseEndGroup();
            break;
          case '\\':
            $this->ParseControl();
            break;
          default:
            $this->ParseText();
            break;
        }
      }
    }
  }

  class RtfState
  {
    public function __construct()
    {
      $this->Reset();
    }

    public function Reset()
    {
      $this->bold = false;
      $this->italic = false;
      $this->underline = false;
      $this->end_underline = false;
      $this->strike = false;
      $this->hidden = false;
      $this->fontsize = 0;
    }
  }

  class RtfHtml
  {
    public function Format($root)
    {
      $this->output = "";
      // Create a stack of states:
      $this->states = array();
      // Put an initial standard state onto the stack:
      $this->state = new RtfState();
      array_push($this->states, $this->state);
      $this->FormatGroup($root);
      return $this->output;
    }

    protected function FormatGroup($group)
    {
      // Can we ignore this group?
      if ($group->GetType() == "fonttbl") return;
      if ($group->GetType() == "colortbl") return;
      if ($group->GetType() == "stylesheet") return;
      if ($group->GetType() == "info") return;
      // Skip any pictures:
      if (substr($group->GetType(), 0, 4) == "pict") return;
      if ($group->IsDestination()) return;

      // Push a new state onto the stack:
      $this->state = clone $this->state;
      array_push($this->states, $this->state);

      foreach($group->children as $child)
      {
        if(get_class($child) == "RtfGroup") $this->FormatGroup($child);
        if(get_class($child) == "RtfControlWord") $this->FormatControlWord($child);
        if(get_class($child) == "RtfControlSymbol") $this->FormatControlSymbol($child);
        if(get_class($child) == "RtfText") $this->FormatText($child);
      }

      // Pop state from stack.
      array_pop($this->states);
      $this->state = $this->states[sizeof($this->states)-1];
    }

    protected function FormatControlWord($word)
    {
      if($word->word == "plain") $this->state->Reset();
      if($word->word == "b") $this->state->bold = $word->parameter;
      if($word->word == "i") $this->state->italic = $word->parameter;
      if($word->word == "ul") $this->state->underline = $word->parameter;
      if($word->word == "ulnone") $this->state->end_underline = $word->parameter;
      if($word->word == "strike") $this->state->strike = $word->parameter;
      if($word->word == "v") $this->state->hidden = $word->parameter;
      if($word->word == "fs") $this->state->fontsize = ceil(($word->parameter / 24) * 16);

      if($word->word == "par") $this->output .= "<p>";

      // Characters:
      if($word->word == "lquote") $this->output .= "&lsquo;";
      if($word->word == "rquote") $this->output .= "&rsquo;";
      if($word->word == "ldblquote") $this->output .= "&ldquo;";
      if($word->word == "rdblquote") $this->output .= "&rdquo;";
      if($word->word == "emdash") $this->output .= "&mdash;";
      if($word->word == "endash") $this->output .= "&ndash;";
      if($word->word == "bullet") $this->output .= "&bull;";
      if($word->word == "u") $this->output .= "&loz;";
    }

    protected function BeginState()
    {
      $span = "";
      if($this->state->bold) $span .= "font-weight:bold;";
      if($this->state->italic) $span .= "font-style:italic;";
      if($this->state->underline) $span .= "text-decoration:underline;";
      if($this->state->end_underline) $span .= "text-decoration:none;";
      if($this->state->strike) $span .= "text-decoration:strikethrough;";
      if($this->state->hidden) $span .= "display:none;";
      if($this->state->fontsize != 0) $span .= "font-size: {$this->state->fontsize}px;";
      $this->output .= "<span style='{$span}'>";
    }

    protected function EndState()
    {
      $this->output .= "</span>";
    }

    protected function FormatControlSymbol($symbol)
    {
      if($symbol->symbol == '\'')
      {
        $this->BeginState();
        $this->output .= htmlentities(chr($symbol->parameter), ENT_QUOTES, 'ISO-8859-1');
        $this->EndState();
      }
    }

    protected function FormatText($text)
    {
      $this->BeginState();
      $this->output .= $text->text;
      $this->EndState();
    }
  }

To use:

$reader = new RtfReader();
$rtf = file_get_contents("arquivo.rtf"); //Ou o campo que contem a mensagem
$reader->Parse($rtf)

Call for conversion:

$formatter = new RtfHtml();
echo $formatter->Format($reader->root);

For the Desktop you can use a component TWebBrowser that he will read and interpret all HTML.

procedure CarregaMensagem; //Lendo o campo que contem a informação gravada em HTML/RTF
begin      
  if (Pos('{\rtf1\',Copy(CAMPO_QUE_CONTEM_A_INFORMAÇÃO, 1, 7)) > 0) then
    //Carrega com TRichEdit
  else
    //Carrega com TWebBrowser
end;

If it is not feasible to modify the two platforms, you can still save the HTML in RTF. Follow Question and Answer on Soen.

1

Your question makes me understand that it is about Delphi and html should be just the end result. Can not understand well and what the goal, if Delphi is mandatory or can be an application in html even, since you used the tag .

If what you want is to port the document . rtf to web platforms but still want to be able to use with your Delphi application then you can use the https://github.com/tbluemel/rtf.js which is javascript, see an example with Drag-and-Drop and Filereader API:

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
<script src="jquery.binarytransport.js"></script>
<link rel="stylesheet" href="jquery.svg.css">
<script src="jquery.svg.min.js"></script>
<script src="../rtf.js"></script>
<script src="../wmf.js"></script>
<script>
function closeDoc(reset) {
    $("#havemeta").hide();
    $("#meta").empty();
    $("#content").empty();
    $("#dropzone").show();
    $("#closebutton").hide();
    $("#tools").hide();
    if (reset) {
        $("#samplecombo").val("");
    }
}
function beginLoading() {
    closeDoc(false);
    $("#dropzone").hide();
    $("#content").text("Loading...");
}
function setPictBorder(elem, show) {
    return elem.css("border", show ? "1px dotted red" : "none");
}
function setUnsafeLink(elem, warn) {
    return elem.css("border", warn ? "1px dashed red" : "none");
}
function displayRtfFile(blob) {
    try {
        var showPicBorder = $("#showpicborder").prop("checked");
        var warnHttpLinks = $("#warnhttplink").prop("checked");
        var settings = {
            onPicture: function(create) {
                var elem = create().attr("class", "rtfpict"); // WHY does addClass not work on <svg>?!
                return setPictBorder(elem, showPicBorder);
            },
            onHyperlink: function(create, hyperlink) {
                var url = hyperlink.url();
                var lnk = create();
                if (url.substr(0, 7) == "http://") {
                    // Wrap http:// links into a <span>
                    var span = setUnsafeLink($("<span>").addClass("unsafelink").append(lnk), warnHttpLinks);
                    span.click(function(evt) {
                        if ($("#warnhttplink").prop("checked")) {
                            evt.preventDefault();
                            alert("Unsafe link: " + url);
                            return false;
                        }
                    });
                    return {
                        content: lnk,
                        element: span
                    };
                } else {
                    return {
                        content: lnk,
                        element: lnk
                    };
                }
            },
        };
        var doc = new RTFJS.Document(blob, settings);
        var haveMeta = false;
        var meta = doc.metadata();
        for (var prop in meta) {
            $("#meta").append($("<div>").append($("<span>").text(prop + ": ")).append($("<span>").text(meta[prop].toString())));
            haveMeta = true;
        }
        if (haveMeta)
            $("#havemeta").show();
        $("#content").empty().append(doc.render());
        $("#closebutton").show();
        $("#tools").show();
        console.log("All done!");
    } catch(e) {
        if (e instanceof RTFJS.Error)
            $("#content").text("Error: " + e.message);
        else
            throw e;
    }
}
function loadRtfFile(file) {
    beginLoading();
    $.ajax({
        url: file,
        dataType: "binary",
        processData: false,
        success: function(result) {
            var reader = new FileReader();
            reader.onload = function(evt) {
                displayRtfFile(evt.target.result);
            };
            reader.readAsArrayBuffer(result);
        },
        error: function(jqXHR, textStatus, errorThrown) {
            $("#content").text("Error: " + errorThrown);
        }
    });
}
$(document).ready(function() {
    $("#closebutton").click(function() {
        closeDoc(true);
    });

    $("#dropzone")
        .on("drop", function(evt) {
            evt.stopPropagation()
            evt.preventDefault();

            var files = evt.originalEvent.dataTransfer.files;
            if (files.length > 1) {
                alert("Please only drop one file!");
            } else {
                var reader = new FileReader();
                reader.onload = function(evt) {
                    beginLoading();
                    setTimeout(function() {
                        displayRtfFile(evt.target.result);
                    }, 100);
                };
                reader.readAsArrayBuffer(files[0]);
            }
        })
        .on("dragover", function(evt) {
            evt.stopPropagation()
            evt.preventDefault();
        });
    closeDoc(true);
});
</script>
</head>
<body>
<div style="margin: 4pt;">
    <span>
        <form>
            <input id="closebutton" type="button" value="Fechar" style="display: none;"/>
        </form>
    </span>
</div>
<div id="dropzone" style="display: inline-block; border-radius: 6pt; border: 2pt solid #dddddd; padding: 30pt;">
    Arraste e solte um arquivo RTF aqui
</div>
<div style="background-color:#f0ffff;display:none;" id="havemeta">
    <div>Metadata:</div>
    <div id="meta"></div>
</div>
<div id="content"></div>
</body>
</html>

You can use on #content the parameter contenteditable="true" and so you can edit the document via html, but you have to convert it back to . rtf, as it will be in . html so far.

To understand the basics of its use is:

var doc = new RTFJS.Document(blob, settings);
document.getElementById("meu-editor").innerHTML = doc.render();

or

var doc = new RTFJS.Document(blob, settings);
document.getElementById("meu-editor").value = doc.render();

It will depend on the type of editor you use.

  • 1

    Next, I don’t see any way to clarify my question, I believe you’re going in the wrong context, but come on, I have a web application that has a text editor, in case it uses bootstrap, not that bootstrap is anything relevant, I need to write texts in RTF and read in HTML is this, one of the questions you might have would be, why save in RTF? why there is another application (Delphi) that can only create texts and display them in RTF.

  • Junior Moreira was able to understand my intention and indicated to me, since you have not found any editor or library that converts back and forth into RTF that I save into HTML and add an if in Delphi to identify if it is html to instead of showing a memo show a Webview and on the web, if RTF do convert to HTML so it can display.

  • @Highlander This is exactly what I’m talking about, one minute you’re talking about a bootstrap text editor (which is supposed to be html+javascript) and the next you’re talking about the Delphi editor TRichEdit. So it gets really hard to understand, I think my answer should help you, if what I understand is that you already have it. rtf generated by the application written in Delphi, so you want to convert to html, am I right? So this is just what the new RTFJS.Document(blob, settings); makes. Now if this doesn’t resolve, I must assume that the javascript tag has nothing to do with the question....

  • 1

    ... So the solution you’re looking for is 100% in Delphi. @Highlander

  • @Highlander anyway I edited the question. Note that the drag-and-drop code is just an example to see how the library works. What matters is you use the new RTFJS.Document(blob, settings);. But if the solution doesn’t fit because it’s javascript, then I recommend that you edit the question and use the tags that are only relevant to the problem. Because if javascript does not serve then it must be solved with some lib for even Delphi or some parse in objectpascal. Please don’t get me wrong, they’re just tips, always assume good intention: http://answall.com/help/be-nice ;)

Browser other questions tagged

You are not signed in. Login or sign up in order to post.