{ String manipulation routines.
  Copyright (C) 2006 - 2018 Aleg Azarouski.
}

unit KDL.StringUtils;
//{$I NoRTTI}
{$IFDEF UNICODE}
{$WEAKLINKRTTI ON}
{$RTTI EXPLICIT METHODS([]) PROPERTIES([]) FIELDS([])}
{$ENDIF}

interface

const
  ListDivider = #13; // Used in DelphiToStringEx
  defHumanizeDivider = '\'; // Used in StringToHumanized
  defHumanizedLF = '\#10'; 
  UnicodeLabel: array[0..2] of Byte = ($EF, $BB, $BF);

// Split a string by a given divider
// Return in El - left part, in s - rest of a string
procedure SplitBy(var S: string; const Divider: string; var Elem: string);
// Escaped string to string (based on the JEDI Code Library (JCL))
function StrEscapedToString(const S: string): string;
// Encode string to lng file string
// DividerCR - substitution for #13
// DividerCRLF - substitution for #13#10
// DividerLF - substitution for #10
function StringToLng(const S: string; Humanize: Boolean; const DividerCR,
  DividerCRLF, DividerLF: string): string;
// Decode lng file string to string
// DividerCR - substitution for #13
// DividerCRLF - substitution for #13#10
// DividerLF - substitution for #10
// DefaultLineBreak - used when DividerCR = DividerCRLF
function LngToString(const S: string; Humanize: Boolean; const DividerCR,
  DividerCRLF, DividerLF, DefaultLineBreak: string): string;

implementation

uses
  SysUtils, StrUtils;

{$region 'Delphi string conversions'}
procedure SplitBy(var S: string; const Divider: string; var Elem: string);
var
  i: Integer;
begin
  i := Pos(Divider, S);
  if i <= 0 then begin
    Elem := S;
    S := '';
  end else begin
    Elem := Copy(S, 1, i-1);
    Delete(S, 1, i + Length(Divider) - 1);
  end;
end;

// Encode string to delphi style string
function StringToDelphi(const s: string): string;
var
  i: Integer;
  insideStr: Boolean;
  res: string;
  ch: Char;

  procedure SwitchStr;  // '...'
  begin
    insideStr := not insideStr;
    res := res + '''';
  end;

begin
  insideStr := False;
  if s = '' then
    res := ''''''
  else begin
    for i := 1 to Length(s) do begin
      ch := s[i];
      case ch of
        '''': begin
          if insideStr then
            res := res + ''''''
          else begin
            res := res + '''''''';
            insideStr := True;
          end;
        end;
        #0..#31: begin
          if insideStr then SwitchStr;
          res := res + '#' + IntToStr(ord(ch));
        end;
        else begin
          if not insideStr then SwitchStr;
          res := res + ch;
        end;
      end;
    end;
  end;
  if insideStr then SwitchStr;
  Result := res;
end;

type
  EDelphiToStringError = Class(Exception)
  public
    iBadChar: Integer; // Bad character position
  end;

// Decode delphi style string to string
function DelphiToString(const s: string): string;
label
  Err;
var
  i, iOpened: Integer;
  res: string;
  insideStr, insideCode: Boolean;
  ch: Char;

  procedure OpenStr;  // '...
  begin
    insideStr := True;
    iOpened := i;
  end;

  procedure OpenCode; // #13
  begin
    insideCode := True;
    iOpened := i;
  end;

  function CloseCode: Boolean;
  begin
    try
      res := res + Char(StrToInt(Copy(s, iOpened+1, i-iOpened-1)));
      Result := True;
      insideCode := False;
    except
      Result := False;
    end;
  end;

var
  Ex: EDelphiToStringError;
begin
  res := '';
  insideStr := False;
  insideCode := False;
  // 'Method ''%s'' not supported by automation object'
  // 'Exception %s in module %s at %p.'#13#10'%s%s'#13#10
  // '''hallo'     --     'hallo'''
  // 'hal'''#13#10'lo'     --       'hallo''hallo'
  for i := 1 to Length(s) do begin
    ch := s[i];
    if insideStr then begin
      // Str opened, code closed
      if ch = '''' then
        insideStr := False
      else
        res := res + ch;
    end else begin
      if insideCode then begin
        // Str closed, code opened
        case ch of
          '''': begin
            if not CloseCode then
              goto Err;
            OpenStr;
          end;
          '#': begin
            if not CloseCode then
              goto Err;
            OpenCode;
          end;
          '0'..'9':;
          else goto Err;
        end;
      end else begin
        // Str closed, code closed
        case ch of
          '''': begin
            if (i > 1) and (s[i-1] = '''') then
              res := res + '''';
            OpenStr;
          end;
          '#': OpenCode;
          else begin
            Result := res;
            Ex := EDelphiToStringError.Create('Bad decoded string: "' + s + '"');
            Ex.iBadChar := i;
            raise Ex;
          end;
        end;
      end;
    end;
  end;
  if insideStr then begin
    Err:
    raise Exception.Create('Bad decoded string: "' + s + '"');
  end;
  if insideCode then CloseCode;
  Result := res;
end;

// Decode delphi style string and stringlist to string
// Stringlist elements delimited by #13
function DelphiToStringEx(s: string): string;
var
  res, s1: string;

  procedure AddResS1;
  begin
    if res <> '' then
      res := res + ListDivider;
    res := res + S1;
  end;

var
  Ok: Boolean;
begin
  res := '';
  repeat
    Ok := True;
    try
      s1 := DelphiToString(s);
    except
      on E: EDelphiToStringError do begin
        AddResS1;
        s := Trim(Copy(s, E.iBadChar+1, MaxInt));
        Ok := False;
      end;
    end;
  until Ok;
  AddResS1;
  Result := res;
end;
{$endregion}

{$region 'Escaped string conversions'}
function StrEscapedToString(const S: string): string;

  // \x041f --> wide character
  procedure HandleHexEscapeSeq(const S: string; var I: Integer; Len: Integer;
    var Dest: string);
  const
    hexDigits = string('0123456789abcdefABCDEF');
  var
    startI, val, n: Integer;
  begin
    startI := I;
    val := 0;
    while I < StartI + 4 do begin
      n := Pos(S[I+1], hexDigits) - 1;
      if n < 0 then begin
        if startI = I then begin
          // '\x' without hex digit following is not escape sequence
          Dest := Dest + '\x';
          Exit;
        end;
      end else begin
        Inc(I);
        if n >= 16 then
          n := n - 6;
        val := val * 16 + n;
        if val > Ord(High(Char)) then
          raise Exception.CreateFmt(
            'Numeric constant too large (%d) at position %d.', [val, startI]);
      end;
    end;
    Dest := Dest + Char(val);
  end;

  procedure HandleOctEscapeSeq(const S: string; var I: Integer; Len: Integer;
    var Dest: string);
  const
    octDigits = string('01234567');
  var
    startI, val, n: Integer;
  begin
    startI := I;
    // first digit
    val := Pos(S[I], octDigits) - 1;
    if I < Len then
    begin
      n := Pos(S[I + 1], octDigits) - 1;
      if n >= 0 then
      begin
        Inc(I);
        val := val * 8 + n;
      end;
      if I < Len then
      begin
        n := Pos(S[I + 1], octDigits) - 1;
        if n >= 0 then
        begin
          Inc(I);
          val := val * 8 + n;
        end;
      end;
    end;

    if val > Ord(High(Char)) then
      raise Exception.CreateFmt(
        'Numeric constant too large (%d) at position %d.', [val, startI]);

    Dest := Dest + Char(val);
  end;

const
  NativeBell = Char(#7);
  NativeBackspace = Char(#8);
  NativeTab = Char(#9);
  NativeLineFeed = Char(#10);
  NativeVerticalTab = Char(#11);
  NativeFormFeed = Char(#12);
  NativeCarriageReturn = Char(#13);
var
  I, Len: Integer;
begin
  Result := '';
  I := 1;
  Len := Length(S);
  while I <= Len do begin
    if not ((S[I] = '\') and (I < Len)) then
      Result := Result + S[I]
    else begin
      Inc(I); // Jump over escape character
      case S[I] of
        'a':
          Result := Result + NativeBell;
        'b':
          Result := Result + NativeBackspace;
        'f':
          Result := Result + NativeFormFeed;
        'n':
          Result := Result + NativeLineFeed;
        'r':
          Result := Result + NativeCarriageReturn;
        't':
          Result := Result + NativeTab;
        'v':
          Result := Result + NativeVerticalTab;
        '\':
          Result := Result + '\';
        '"':
          Result := Result + '"';
        '''':
          Result := Result + ''''; // Optionally escaped
        '?':
          Result := Result + '?';  // Optionally escaped
        'x':
          if I < Len then
            // Start of hex escape sequence
            HandleHexEscapeSeq(S, I, Len, Result)
          else
            // '\x' at end of string is not escape sequence
            Result := Result + '\x';
        '0'..'7':
          // start of octal escape sequence
          HandleOctEscapeSeq(S, I, Len, Result);
      else
        // no escape sequence
        Result := Result + '\' + S[I];
      end;
    end;
    Inc(I);
  end;
end;
{$endregion}

{$region 'Humanized string conversions'}
// Encode string to "humanized" style string
// DividerCR - substitution for #13
// DividerCRLF - substitution for #13#10
// DividerLF - substitution for #10
function StringToHumanized(const s: string; const DividerCR, DividerCRLF,
  DividerLF: string): string;
begin
  if (Pos(DividerCR, s) > 0)
    or (Pos(DividerCRLF, s) > 0)
    or (Pos(DividerLF, s) > 0)
  then
    raise Exception.CreateFmt(
      'String "%s" contains a humanize divider "%s", "%s" or "%s" and can''t'
      + ' be converted properly.'#13#10
      + 'Try set a different string as the divider for this application.',
      [s, DividerCR, DividerCRLF, DividerLF]);
  Result := StringReplace(
    StringReplace(
      StringReplace(s, sLineBreak, DividerCRLF, [rfReplaceAll]),
      #13, DividerCR, [rfReplaceAll]),
    #10, DividerLF, [rfReplaceAll]);
end; 

// Decode "humanized" style string to string
// DividerCR - substitution for #13
// DividerCRLF - substitution for #13#10
// DividerLF - substitution for #10
// DefaultLineBreak - used when DividerCR = DividerCRLF
function HumanizedToString(const s: string; const DividerCR, DividerCRLF,
  DividerLF, DefaultLineBreak: string): string;
begin
  if DividerCR = DividerCRLF then
    Result := StringReplace(s, DividerCR, DefaultLineBreak, [rfReplaceAll])
  else begin
    Result := StringReplace(s, DividerCR, #13, [rfReplaceAll]);
    Result := StringReplace(Result, DividerCRLF, sLineBreak, [rfReplaceAll]);
  end;
  Result := StringReplace(Result, DividerLF, #10, [rfReplaceAll]);
end;
{$endregion}

{$region 'Lng file string conversions'}
function StringToLng(const S: string; Humanize: Boolean; const DividerCR,
  DividerCRLF, DividerLF: string): string;
begin
  if Humanize then
    Result := StringToHumanized(S, DividerCR, DividerCRLF, DividerLF)
  else
    Result := StringToDelphi(S);
end;

function LngToString(const S: string; Humanize: Boolean; const DividerCR,
  DividerCRLF, DividerLF, DefaultLineBreak: string): string;
begin
  if Humanize then
    Result := HumanizedToString(S, DividerCR, DividerCRLF, DividerLF,
      DefaultLineBreak)
  else
    Result := DelphiToStringEx(S);
end;
{$endregion}

end.
