unit exportgrid;

{$mode delphi}{$H+}

interface

uses
  SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls, ExtCtrls, Menus, ComCtrls, laz.VirtualTrees, SynExportHTML,
  extra_controls, dbstructures, lazaruscompat, RegExpr, StrUtils, comboex, EditBtn, Buttons, LCLIntf,
  extfiledialog, generic_types;

type
  TGridExportFormat = (
    efExcel,
    efCSV,
    efHTML,
    efXML,
    efSQLInsert,
    efSQLInsertIgnore,
    efSQLReplace,
    efSQLDeleteInsert,
    efSQLUpdate,
    efLaTeX,
    efTextile,
    efJiraTextile,
    efPHPArray,
    efMarkDown,
    efJSON,
    efJSONLines
    );

  { TfrmExportGrid }

  TfrmExportGrid = class(TExtForm)
    btnOK: TButton;
    btnCancel: TButton;
    chkFocusedColumnOnly: TCheckBox;
    grpSelection: TRadioGroup;
    grpOutput: TGroupBox;
    radioOutputCopyToClipboard: TRadioButton;
    radioOutputFile: TRadioButton;
    editFilename: TEditButton;
    grpOptions: TGroupBox;
    chkIncludeColumnNames: TCheckBox;
    editSeparator: TEditButton;
    editEncloser: TEditButton;
    editTerminator: TEditButton;
    lblSeparator: TLabel;
    lblEncloser: TLabel;
    lblTerminator: TLabel;
    popupCSVchar: TPopupMenu;
    menuCSVtab: TMenuItem;
    menuCSVunixlinebreak: TMenuItem;
    menuCSVmaclinebreak: TMenuItem;
    menuCSVwinlinebreak: TMenuItem;
    menuCSVnul: TMenuItem;
    menuCSVbackspace: TMenuItem;
    menuCSVcontrolz: TMenuItem;
    comboEncoding: TComboBox;
    lblEncoding: TLabel;
    popupRecentFiles: TPopupMenu;
    menuCSVsinglequote: TMenuItem;
    menuCSVdoublequote: TMenuItem;
    menuCSVcomma: TMenuItem;
    menuCSVsemicolon: TMenuItem;
    N1: TMenuItem;
    N2: TMenuItem;
    N3: TMenuItem;
    chkIncludeAutoIncrement: TCheckBox;
    chkIncludeQuery: TCheckBox;
    lblNull: TLabel;
    editNull: TEditButton;
    btnSetClipboardDefaults: TSpeedButton;
    chkRemoveLinebreaks: TCheckBox;
    grpFormat: TGroupBox;
    comboFormat: TComboBoxEx;
    chkOpenFile: TCheckBox;
    procedure FormCreate(Sender: TObject);
    procedure CalcSize(Sender: TObject);
    procedure FormClose(Sender: TObject; var Action: TCloseAction);
    procedure editFilenameRightButtonClick(Sender: TObject);
    procedure editFilenameChange(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
    procedure popupRecentFilesPopup(Sender: TObject);
    procedure menuCSVClick(Sender: TObject);
    procedure editCSVRightButtonClick(Sender: TObject);
    procedure editCSVChange(Sender: TObject);
    procedure ValidateControls(Sender: TObject);
    procedure btnOKClick(Sender: TObject);
    procedure FormShow(Sender: TObject);
    procedure comboFormatSelect(Sender: TObject);
    procedure btnSetClipboardDefaultsClick(Sender: TObject);
  private
    { Private declarations }
    FCSVEditor: TEditButton;
    FCSVSeparator, FCSVEncloser, FCSVTerminator, FCSVNull: String;
    FGrid: TLazVirtualStringTree;
    FRecentFiles: TStringList;
    FHiddenCopyMode: Boolean;
    procedure SaveDialogTypeChange(Sender: TObject);
    function GetExportFormat: TGridExportFormat;
    procedure SetExportFormat(Value: TGridExportFormat);
    procedure SetExportFormatByFilename;
    procedure SelectRecentFile(Sender: TObject);
    procedure PutFilenamePlaceholder(Sender: TObject);
    function FormatCsv(Text, Encloser: String; DataType: TDBDatatype; SubFormat: TGridExportFormat): String;
    function FormatJson(Text: String): String;
    function FormatPhp(Text: String): String;
    function FormatLatex(Text: String): String;
  public
    { Public declarations }
    const FormatToFileExtension: Array[TGridExportFormat] of String =
      (
        ('csv'),
        ('csv'),
        ('html'),
        ('xml'),
        ('sql'),
        ('sql'),
        ('sql'),
        ('sql'),
        ('sql'),
        ('LaTeX'),
        ('textile'),
        ('jira-textile'),
        ('php'),
        ('md'),
        ('json'),
        ('jsonl')
        );
    const FormatToDescription: Array[TGridExportFormat] of String =
      (
        ('Excel CSV'),
        ('Delimited text'),
        ('HTML table'),
        ('XML'),
        ('SQL INSERTs'),
        ('SQL INSERT IGNOREs'),
        ('SQL REPLACEs'),
        ('SQL DELETEs/INSERTs'),
        ('SQL UPDATEs'),
        ('LaTeX'),
        ('Textile'),
        ('Jira Textile'),
        ('PHP Array'),
        ('Markdown Here'),
        ('JSON'),
        ('JSON Lines')
        );
    const CopyAsActionPrefix = 'actCopyAs';
    property Grid: TLazVirtualStringTree read FGrid write FGrid;
    property ExportFormat: TGridExportFormat read GetExportFormat write SetExportFormat;
  end;


implementation

uses main, apphelpers, dbconnection;

{$R *.lfm}



procedure TfrmExportGrid.FormCreate(Sender: TObject);
var
  ef: TGridExportFormat;
  SenderName: String;
  comboItem: TComboExItem;
begin
  Width := AppSettings.ReadInt(asGridExportWindowWidth);
  Height := AppSettings.ReadInt(asGridExportWindowHeight);
  editFilename.Text := AppSettings.ReadString(asGridExportFilename);
  FRecentFiles := Explode(DELIM, AppSettings.ReadString(asGridExportRecentFiles));
  comboEncoding.Items.Assign(MainForm.FileEncodings);
  comboEncoding.Items.Delete(0); // Remove "Auto detect"
  comboEncoding.ItemIndex := AppSettings.ReadInt(asGridExportEncoding);
  comboFormat.Items.Clear;
  comboFormat.ItemsEx.Clear;
  for ef:=Low(TGridExportFormat) to High(TGridExportFormat) do begin
    comboItem := TComboExItem.Create(comboFormat.ItemsEx);
    comboItem.Caption := FormatToDescription[ef];
    comboItem.ImageIndex := GetFileExtImageIndex(FormatToFileExtension[ef]);
  end;
  SenderName := Owner.Name;
  FHiddenCopyMode := SenderName.StartsWith(CopyAsActionPrefix);

  if FHiddenCopyMode then begin
    radioOutputCopyToClipboard.Checked := True;
    comboFormat.ItemIndex := Owner.Tag;
    grpSelection.ItemIndex := 0; // Always use selected cells in copy mode
    chkIncludeColumnNames.Checked := AppSettings.ReadBool(asGridExportClpColumnNames);
    chkIncludeAutoIncrement.Checked := AppSettings.ReadBool(asGridExportClpIncludeAutoInc);
    chkFocusedColumnOnly.Checked := False;
    chkIncludeQuery.Checked := False; // Always off in copy mode
    chkRemoveLinebreaks.Checked := AppSettings.ReadBool(asGridExportClpRemoveLinebreaks);
    chkOpenFile.Checked := False; // Always off in copy mode
    FCSVSeparator := AppSettings.ReadString(asGridExportClpSeparator);
    FCSVEncloser := AppSettings.ReadString(asGridExportClpEncloser);
    FCSVTerminator := AppSettings.ReadString(asGridExportClpTerminator);
    FCSVNull := AppSettings.ReadString(asGridExportClpNull);
  end else begin
    radioOutputCopyToClipboard.Checked := AppSettings.ReadBool(asGridExportOutputCopy);
    radioOutputFile.Checked := AppSettings.ReadBool(asGridExportOutputFile);
    comboFormat.ItemIndex := AppSettings.ReadInt(asGridExportFormat);
    grpSelection.ItemIndex := AppSettings.ReadInt(asGridExportSelection);
    chkIncludeColumnNames.Checked := AppSettings.ReadBool(asGridExportColumnNames);
    chkIncludeAutoIncrement.Checked := AppSettings.ReadBool(asGridExportIncludeAutoInc);
    chkFocusedColumnOnly.Checked := AppSettings.ReadBool(asGridExportFocusedColumnOnly);
    chkIncludeQuery.Checked := AppSettings.ReadBool(asGridExportIncludeQuery);
    chkRemoveLinebreaks.Checked := AppSettings.ReadBool(asGridExportRemoveLinebreaks);
    chkOpenFile.Checked := AppSettings.ReadBool(asGridExportOpenFile);
    FCSVSeparator := AppSettings.ReadString(asGridExportSeparator);
    FCSVEncloser := AppSettings.ReadString(asGridExportEncloser);
    FCSVTerminator := AppSettings.ReadString(asGridExportTerminator);
    FCSVNull := AppSettings.ReadString(asGridExportNull);
  end;
  ValidateControls(Sender);
end;


procedure TfrmExportGrid.FormShow(Sender: TObject);
var
  FocusedCol: String;
begin
  // Show dialog. Expect "Grid" property to be set now by the caller.
  chkIncludeAutoIncrement.OnClick := CalcSize;
  chkFocusedColumnOnly.OnClick := CalcSize;
  CalcSize(Sender);
  // Show name of focused column
  if Grid.FocusedColumn > -1 then
    FocusedCol := Grid.Header.Columns[Grid.FocusedColumn].Text
  else
    FocusedCol := '';
  chkFocusedColumnOnly.Caption := f_('Only focused column (%s)', [FocusedCol]);
  chkFocusedColumnOnly.Enabled := not FocusedCol.IsEmpty;
end;


procedure TfrmExportGrid.FormClose(Sender: TObject; var Action: TCloseAction);
begin
  // Store settings
  if ModalResult = mrOK then begin
    AppSettings.WriteBool(asGridExportOutputCopy, radioOutputCopyToClipboard.Checked);
    AppSettings.WriteBool(asGridExportOutputFile, radioOutputFile.Checked);
    AppSettings.WriteString(asGridExportFilename, editFilename.Text);
    AppSettings.WriteString(asGridExportRecentFiles, Implode(DELIM, FRecentFiles));
    AppSettings.WriteInt(asGridExportEncoding, comboEncoding.ItemIndex);
    AppSettings.WriteInt(asGridExportFormat, comboFormat.ItemIndex);
    AppSettings.WriteInt(asGridExportSelection, grpSelection.ItemIndex);
    AppSettings.WriteBool(asGridExportColumnNames, chkIncludeColumnNames.Checked);
    AppSettings.WriteBool(asGridExportIncludeAutoInc, chkIncludeAutoIncrement.Checked);
    AppSettings.WriteBool(asGridExportFocusedColumnOnly, chkFocusedColumnOnly.Checked);
    AppSettings.WriteBool(asGridExportIncludeQuery, chkIncludeQuery.Checked);
    AppSettings.WriteBool(asGridExportRemoveLinebreaks, chkRemoveLinebreaks.Checked);
    AppSettings.WriteBool(asGridExportOpenFile, chkOpenFile.Checked);
    AppSettings.WriteString(asGridExportSeparator, FCSVSeparator);
    AppSettings.WriteString(asGridExportEncloser, FCSVEncloser);
    AppSettings.WriteString(asGridExportTerminator, FCSVTerminator);
    AppSettings.WriteString(asGridExportNull, FCSVNull);
  end;
end;


procedure TfrmExportGrid.ValidateControls(Sender: TObject);
var
  Enable: Boolean;
begin
  // Display the actually used control characters, even if they cannot be changed
  case ExportFormat of
    efExcel: begin
      // Tab for pasting, semicolon if comma is also the decimal separator, and comma for the rest
      // see http://en.wikipedia.org/wiki/Comma-separated_values
      if radioOutputCopyToClipboard.Checked then
        editSeparator.Text := '\t'
      else if FormatSettings.DecimalSeparator=',' then
        editSeparator.Text := ';'
      else
        editSeparator.Text := ',';
      editEncloser.Text := '"';
      editTerminator.Text := '\r\n';
      editNull.Text := FCSVNull;
    end;
    efCSV: begin
      editSeparator.Text := FCSVSeparator;
      editEncloser.Text := FCSVEncloser;
      editTerminator.Text := FCSVTerminator;
      editNull.Text := FCSVNull;
    end;
    efMarkDown:
      editNull.Text := FCSVNull;
    else begin
      editSeparator.Text := '';
      editEncloser.Text := '';
      editTerminator.Text := '';
      editNull.Text := '';
    end;
  end;

  chkIncludeQuery.Enabled := ExportFormat in [efHTML, efXML, efMarkDown, efJSON];
  chkOpenFile.Enabled := radioOutputFile.Checked;
  Enable := ExportFormat = efCSV;
  lblSeparator.Enabled := Enable;
  editSeparator.Enabled := Enable;
  //editSeparator.RightButton.Enabled := Enable;
  lblEncloser.Enabled := Enable;
  editEncloser.Enabled := Enable;
  //editEncloser.RightButton.Enabled := Enable;
  lblTerminator.Enabled := Enable;
  editTerminator.Enabled := Enable;
  //editTerminator.RightButton.Enabled := Enable;
  lblNull.Enabled := ExportFormat in [efExcel, efCSV, efMarkDown];
  editNull.Enabled := lblNull.Enabled;
  //editNull.RightButton.Enabled := lblNull.Enabled;
  btnOK.Enabled := radioOutputCopyToClipboard.Checked or (radioOutputFile.Checked and (editFilename.Text <> ''));
  if radioOutputFile.Checked then
    editFilename.Font.Color := GetThemeColor(clWindowText)
  else
    editFilename.Font.Color := GetThemeColor(clGrayText);
  comboEncoding.Enabled := radioOutputFile.Checked;
  lblEncoding.Enabled := radioOutputFile.Checked;
end;


function TfrmExportGrid.GetExportFormat: TGridExportFormat;
begin
  // This is slow, don't use in large loops
  Result := TGridExportFormat(comboFormat.ItemIndex);
end;


procedure TfrmExportGrid.SetExportFormat(Value: TGridExportFormat);
begin
  comboFormat.ItemIndex := Integer(Value);
  ValidateControls(Self);
end;


procedure TfrmExportGrid.comboFormatSelect(Sender: TObject);
var
  Filename: String;
begin
  // Auto-modify file extension when selecting export format
  // Be careful about triggering editFilename.OnChange event, as we may have come here from that event!
  if radioOutputFile.Checked then begin
    Filename := ExtractFilePath(editFilename.Text) +
      GetFileNameWithoutExtension(editFilename.Text) +
      '.' + FormatToFileExtension[ExportFormat];
    if CompareText(Filename, editFilename.Text) <> 0 then
      editFilename.Text := Filename;
  end;
  ValidateControls(Sender);
end;


procedure TfrmExportGrid.SetExportFormatByFilename;
var
  ext: String;
  efrm: TGridExportFormat;
begin
  // Set format by file extension
  ext := LowerCase(Copy(ExtractFileExt(editFilename.Text), 2, 10));
  for efrm :=Low(TGridExportFormat) to High(TGridExportFormat) do begin
    if ext = FormatToFileExtension[ExportFormat] then
      break;
    if ext = FormatToFileExtension[efrm] then begin
      ExportFormat := efrm;
      break;
    end;
  end;
end;


procedure TfrmExportGrid.editFilenameChange(Sender: TObject);
begin
  radioOutputFile.Checked := True;
end;

procedure TfrmExportGrid.FormDestroy(Sender: TObject);
begin
  AppSettings.WriteInt(asGridExportWindowWidth, ScaleFormToDesign(Width));
  AppSettings.WriteInt(asGridExportWindowHeight, ScaleFormToDesign(Height));
end;


procedure TfrmExportGrid.editFilenameRightButtonClick(Sender: TObject);
var
  Dialog: TExtFileSaveDialog;
  ef: TGridExportFormat;
  Filename: String;
begin
  // Select file target
  Dialog := TExtFileSaveDialog.Create(Self);
  Filename := GetOutputFilename(editFilename.Text, MainForm.ActiveDbObj);
  Dialog.InitialDir := ExtractFilePath(Filename);
  Dialog.FileName := GetFileNameWithoutExtension(Filename);
  for ef:=Low(TGridExportFormat) to High(TGridExportFormat) do
    Dialog.AddFileType('*.'+FormatToFileExtension[ef], FormatToDescription[ef]);
  Dialog.AddFileType('*.*', _('All files'));
  Dialog.OnTypeChange := SaveDialogTypeChange;
  Dialog.FilterIndex := comboFormat.ItemIndex;
  Dialog.OnTypeChange(Dialog);
  if Dialog.Execute then begin
    editFilename.Text := Dialog.FileName;
    SetExportFormatByFilename;
  end;
  Dialog.Free;
end;


procedure TfrmExportGrid.popupRecentFilesPopup(Sender: TObject);
var
  Filename: String;
  Menu: TPopupMenu;
  Item: TMenuItem;
  Placeholders: TStringList;
  i: Integer;
begin
  // Clear and populate drop down menu with recent files and filename placeholders
  Menu := Sender as TPopupMenu;
  Menu.Items.Clear;

  for Filename in FRecentFiles do begin
    Item := TMenuItem.Create(Menu);
    Menu.Items.Add(Item);
    Item.Caption := Filename;
    Item.Hint := Filename;
    Item.OnClick := SelectRecentFile;
    Item.Checked := Filename = editFilename.Text;
  end;

  Item := TMenuItem.Create(Menu);
  Menu.Items.Add(Item);
  Item.Caption := '-';

  Placeholders := GetOutputFilenamePlaceholders;
  for i:=0 to Placeholders.Count-1 do begin
    Item := TMenuItem.Create(Menu);
    Menu.Items.Add(Item);
    Item.Caption := '%' + Placeholders.Names[i] + ': ' + Placeholders.ValueFromIndex[i];
    Item.Hint := '%' + Placeholders.Names[i];
    Item.OnClick := PutFilenamePlaceholder;
  end;

  Placeholders.Free;
end;


procedure TfrmExportGrid.SelectRecentFile(Sender: TObject);
begin
  // Select file from recently used files
  editFilename.Text := (Sender as TMenuItem).Hint;
  SetExportFormatByFilename;
end;


procedure TfrmExportGrid.PutFilenamePlaceholder(Sender: TObject);
begin
  // Put filename placeholder
  editFilename.SelText := (Sender as TMenuItem).Hint;
end;


procedure TfrmExportGrid.btnSetClipboardDefaultsClick(Sender: TObject);
begin
  // Store copy-to-clipboard settings
  AppSettings.ResetPath;
  AppSettings.WriteBool(asGridExportClpColumnNames, chkIncludeColumnNames.Checked);
  AppSettings.WriteBool(asGridExportClpIncludeAutoInc, chkIncludeAutoIncrement.Checked);
  AppSettings.WriteBool(asGridExportRemoveLinebreaks, chkRemoveLinebreaks.Checked);
  AppSettings.WriteString(asGridExportClpSeparator, FCSVSeparator);
  AppSettings.WriteString(asGridExportClpEncloser, FCSVEncloser);
  AppSettings.WriteString(asGridExportClpTerminator, FCSVTerminator);
  AppSettings.WriteString(asGridExportClpNull, FCSVNull);
  MessageDialog(_('Clipboard settings changed.'), mtInformation, [mbOK]);
end;


procedure TfrmExportGrid.CalcSize(Sender: TObject);
var
  GridData: TDBQuery;
  Node: PVirtualNode;
  Col, ExcludeAutoIncCol, IncludeFocusedCol: TColumnIndex;
  ResultCol: Integer;
  RowNum: PInt64;
  SelectedSize, AllSize: Int64;
  CalculatedCount, SelectedCount, AllCount: Int64;
  DoIncludeCol: Boolean;
begin
  GridData := Mainform.GridResult(Grid);
  if not Assigned(GridData) then begin
    MainForm.LogSQL('Failed to get current results');
    Exit;
  end;
  AllSize := 0;
  SelectedSize := 0;
  chkIncludeAutoIncrement.Enabled := (GridData.AutoIncrementColumn > -1) and (not chkFocusedColumnOnly.Checked);
  ExcludeAutoIncCol := -1;
  if chkIncludeAutoIncrement.Enabled and (not chkIncludeAutoIncrement.Checked) then
    ExcludeAutoIncCol := GridData.AutoIncrementColumn;
  IncludeFocusedCol := -1;
  if chkFocusedColumnOnly.Enabled and chkFocusedColumnOnly.Checked then
    IncludeFocusedCol := Grid.FocusedColumn;

  Node := GetNextNode(Grid, nil, False);
  CalculatedCount := 0;
  AllCount := 0;
  SelectedCount := 0;
  while Assigned(Node) do begin
    Inc(AllCount);
    if vsSelected in Node.States then
      Inc(SelectedCount);

    if CalculatedCount < 1000 then begin
      // Performance: use first rows only, and interpolate the rest, see issue #804
      RowNum := Grid.GetNodeData(Node);
      GridData.RecNo := RowNum^;
      Col := Grid.Header.Columns.GetFirstVisibleColumn(True);
      while Col > NoColumn do begin
        ResultCol := Col - 1;
        DoIncludeCol := (Col <> ExcludeAutoIncCol) and
          ((IncludeFocusedCol < 0) or (Col = IncludeFocusedCol));
        if DoIncludeCol then begin
          Inc(AllSize, GridData.ColumnLengths(ResultCol));
          if vsSelected in Node.States then
            Inc(SelectedSize, GridData.ColumnLengths(ResultCol));
        end;
        Col := Grid.Header.Columns.GetNextVisibleColumn(Col);
      end;
      Inc(CalculatedCount);
    end;

    Node := GetNextNode(Grid, Node, False);
  end;
  if AllCount > CalculatedCount then begin
    AllSize := Round(AllSize / CalculatedCount * AllCount);
  end;
  grpSelection.Items[0] := f_('Selection (%s rows, %s)', [FormatNumber(SelectedCount), FormatByteNumber(SelectedSize)]);
  grpSelection.Items[1] := f_('Complete (%s rows, %s)', [FormatNumber(AllCount), FormatByteNumber(AllSize)]);
end;


procedure TfrmExportGrid.editCSVChange(Sender: TObject);
var
  Edit: TEditButton;
begin
  // Remember csv settings
  Edit := Sender as TEditButton;
  case ExportFormat of
    efExcel, efMarkDown: begin
      if Edit = editNull then             FCSVNull := Edit.Text;
    end;
    efCSV: begin
      if Edit = editSeparator then        FCSVSeparator := Edit.Text
      else if Edit = editEncloser then    FCSVEncloser := Edit.Text
      else if Edit = editTerminator then  FCSVTerminator := Edit.Text
      else if Edit = editNull then        FCSVNull := Edit.Text;
    end;
  end;
end;


procedure TfrmExportGrid.SaveDialogTypeChange(Sender: TObject);
var
  Dialog: TExtFileSaveDialog;
  ef: TGridExportFormat;
begin
  // Set default file-extension of saved file and options on the dialog to show
  Dialog := Sender as TExtFileSaveDialog;
  for ef:=Low(TGridExportFormat) to High(TGridExportFormat) do begin
    if Dialog.FilterIndex = Integer(ef) then
      Dialog.DefaultExt := FormatToFileExtension[ef];
  end;
end;


procedure TfrmExportGrid.editCSVRightButtonClick(Sender: TObject);
var
  Item: TMenuItem;
begin
  // Remember editor and prepare popup menu items
  FCSVEditor := Sender as TEditButton;
  for Item in popupCSVchar.Items do begin
    Item.Checked := FCSVEditor.Text = Item.Hint;
  end;
  ShowPopup(FCSVEditor.Button, popupCSVchar);
end;


procedure TfrmExportGrid.menuCSVClick(Sender: TObject);
begin
  // Insert char from menu
  FCSVEditor.Text := TMenuItem(Sender).Hint;
end;


function TfrmExportGrid.FormatCsv(Text, Encloser: String; DataType: TDBDatatype; SubFormat: TGridExportFormat): String;
begin
  Result := Text;
  // Escape encloser characters inside data per de-facto CSV.
  if not Encloser.IsEmpty then
    Result := StringReplace(Result, Encloser, Encloser+Encloser, [rfReplaceAll]);
  // Remove milliseconds from date/time values, unsupported by Excel. See issue #922
  if (SubFormat = efExcel) and (DataType.Category = dtcTemporal) then begin
    Result := ReplaceRegExpr('\.(\d+)$', Result, '');
  end;
end;


function TfrmExportGrid.FormatJson(Text: String): String;
begin
  // String escaping for PHP output. Incompatible to TDBConnection.EscapeString.
  Result := StringReplace(Text, '\', '\\', [rfReplaceAll]);
  Result := StringReplace(Result, #13, '\r', [rfReplaceAll]);
  Result := StringReplace(Result, #10, '\n', [rfReplaceAll]);
  Result := StringReplace(Result, #9, '\t', [rfReplaceAll]);
  Result := StringReplace(Result, '"', '\"', [rfReplaceAll]);
  Result := '"' + Result + '"';
end;

function TfrmExportGrid.FormatPhp(Text: String): String;
begin
  if Text.IndexOfAny([#10, #13, #9, #11, #27, #12]) > -1 then begin
    // https://www.php.net/manual/it/language.types.string.php#language.types.string.syntax.double
    Result := StringReplace(Text, '\', '\\', [rfReplaceAll]);
    Result := StringReplace(Result, #10, '\n', [rfReplaceAll]);
    Result := StringReplace(Result, #13, '\r', [rfReplaceAll]);
    Result := StringReplace(Result, #9, '\t', [rfReplaceAll]);
    Result := StringReplace(Result, #11, '\v', [rfReplaceAll]);
    Result := StringReplace(Result, #27, '\e', [rfReplaceAll]);
    Result := StringReplace(Result, #12, '\f', [rfReplaceAll]);
    Result := StringReplace(Result, '$', '\$', [rfReplaceAll]);
    Result := StringReplace(Result, '"', '\"', [rfReplaceAll]);
    Result := '"' + Result + '"';
  end else begin
    // https://www.php.net/manual/it/language.types.string.php#language.types.string.syntax.single
    Result := StringReplace(Text, '\', '\\', [rfReplaceAll]);
    Result := StringReplace(Text, '''', '\''', [rfReplaceAll]);
    Result := '''' + Result + '''';
  end;
end;


function TfrmExportGrid.FormatLatex(Text: String): String;
var
  TextChr: Char;
const
  NeedBackslash: TSysCharset = ['_', '$', '%', '&'];
begin
  // String escaping for LaTeX output. Mostly uses backslash. Probably incomplete.
  // See pm from H. Flick
  // See https://tex.stackexchange.com/a/301984
  Result := Text;
  for TextChr in NeedBackslash do begin
    Result := StringReplace(Result, TextChr, '\'+TextChr, [rfReplaceAll]);
  end;
end;


procedure TfrmExportGrid.btnOKClick(Sender: TObject);
var
  Col, ExcludeAutoIncCol, IncludeFocusedCol: TColumnIndex;
  ResultCol: Integer;
  Header, Data, tmp, Encloser, Separator, Terminator, TableName, Filename: String;
  Node: PVirtualNode;
  GridData: TDBQuery;
  SelectionOnly, HasNulls: Boolean;
  i: Integer;
  NodeCount: Cardinal;
  RowNum: PInt64;
  HTML: TStream;
  S: TStringStream;
  Exporter: TSynExporterHTML;
  Encoding: TEncoding;
  Bom: TBytes;
  CurrentExportFormat: TGridExportFormat;

  function DoIncludeCol: Boolean;
  begin
    Result := (Col <> ExcludeAutoIncCol) and
      ((IncludeFocusedCol < 0) or (Col = IncludeFocusedCol))
  end;
begin
  Filename := GetOutputFilename(editFilename.Text, MainForm.ActiveDbObj);

  // Confirmation dialog if file exists
  if radioOutputFile.Checked
    and FileExists(Filename)
    and (MessageDialog(_('File exists'), f_('Overwrite file %s?', [Filename]), mtConfirmation, [mbYes, mbCancel]) = mrCancel)
    then begin
      ModalResult := mrNone;
      Exit;
  end;

  try
    Screen.Cursor := crHourglass;

    SelectionOnly := grpSelection.ItemIndex = 0;
    Mainform.DataGridEnsureFullRows(Grid, SelectionOnly);
    GridData := Mainform.GridResult(Grid);
    if SelectionOnly then
      NodeCount := Grid.SelectedCount
    else
      NodeCount := Grid.RootNodeCount;
    MainForm.EnableProgress(NodeCount);
    try
      TableName := GridData.TableName;
    except
      TableName := _('UnknownTable');
    end;
    ExcludeAutoIncCol := NoColumn;
    if chkIncludeAutoIncrement.Enabled and (not chkIncludeAutoIncrement.Checked) then
      ExcludeAutoIncCol := GridData.AutoIncrementColumn + 1;
    IncludeFocusedCol := NoColumn;
    if chkFocusedColumnOnly.Checked then
      IncludeFocusedCol := Grid.FocusedColumn;
    // Calling (Get)ExportFormat is slow, so we store it in a local variable
    CurrentExportFormat := ExportFormat;

    if radioOutputCopyToClipboard.Checked then
      Encoding := TEncoding.UTF8
    else begin
      Encoding := MainForm.GetEncodingByName(comboEncoding.Text);
      // Add selected file to file list, and sort it onto the top of the list
      i := FRecentFiles.IndexOf(editFilename.Text);
      if i > -1 then
        FRecentFiles.Delete(i);
      FRecentFiles.Insert(0, editFilename.Text);
      for i:=FRecentFiles.Count-1 downto 10 do
        FRecentFiles.Delete(i);
    end;

    // Prepare stream
    // Note that TStringStream + TEncoding.UTF8 do not write a BOM (which is nice),
    // although it has a property TUTF8Encoding.GetPreamble.
    // Now, only newer Excel versions need that BOM, so we add it explicitly here
    // P.S.: Note the boolean/False parameter for OwnsEncoding, so our global encodings are not destroyed after usage
    S := TStringStream.Create('', Encoding, False);
    if radioOutputFile.Checked then begin
      Bom := Encoding.GetPreamble;
      if Length(Bom) > 0 then
        S.WriteBuffer(Bom[0], Length(Bom));
    end;

    Header := '';
    case CurrentExportFormat of
      efHTML: begin
        Header :=
          '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" ' + sLineBreak +
          CodeIndent + '"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">' + sLineBreak + sLineBreak +
          '<html>' + sLineBreak +
          CodeIndent + '<head>' + sLineBreak +
          CodeIndent(2) + '<title>' + TableName + '</title>' + sLineBreak +
          CodeIndent(2) + '<meta name="GENERATOR" content="'+ APPNAME+' '+Mainform.AppVersion + '">' + sLineBreak +
          CodeIndent(2) + '<meta http-equiv="Content-Type" content="text/html; charset='+GetHTMLCharsetByEncoding(Encoding)+'" />' + sLineBreak +
          CodeIndent(2) + '<meta name="color-scheme" content="light dark">' + sLineBreak +
          CodeIndent(2) + '<style type="text/css">' + sLineBreak +
          CodeIndent(3) + 'th, td { vertical-align:top; border:1px solid currentColor; padding:0.25rem 0.5rem; }' + sLineBreak +
          CodeIndent(3) + 'table { border-collapse:collapse; }' + sLineBreak;
        Col := Grid.Header.Columns.GetFirstVisibleColumn(True);
        while Col > NoColumn do begin
          // Right-justify all cells to match the grid on screen.
          if Grid.Header.Columns[Col].Alignment = taRightJustify then
            Header := Header + CodeIndent(3) + '.col' + IntToStr(Col) + ' { text-align:right; }' + sLineBreak;
          Col := Grid.Header.Columns.GetNextVisibleColumn(Col);
        end;
        Header := Header +
          CodeIndent(2) + '</style>' + sLineBreak +
          CodeIndent + '</head>' + sLineBreak + sLineBreak +
          CodeIndent + '<body>' + sLineBreak + sLineBreak;
        if chkIncludeQuery.Checked then
          Header := Header + '<p style="font-family:monospace; white-space:pre;">' + GridData.SQL + '</p>' + CRLF + CRLF;
        Header := Header + CodeIndent(2) + '<table caption="' + TableName + ' (' + inttostr(NodeCount) + ' rows)">' + sLineBreak;
        if chkIncludeColumnNames.Checked then begin
          Header := Header +
            CodeIndent(3) + '<thead>' + sLineBreak +
            CodeIndent(4) + '<tr>' + sLineBreak;
          Col := Grid.Header.Columns.GetFirstVisibleColumn(True);
          while Col > NoColumn do begin
            if DoIncludeCol then
              Header := Header + CodeIndent(5) + '<th class="col' + IntToStr(Col) + '">' + Grid.Header.Columns[Col].Text + '</th>' + sLineBreak;
            Col := Grid.Header.Columns.GetNextVisibleColumn(Col);
          end;
          Header := Header +
            CodeIndent(4) + '</tr>' + sLineBreak +
            CodeIndent(3) + '</thead>' + sLineBreak;
        end;
        Header := Header + CodeIndent(3) + '<tbody>' + sLineBreak;
      end;

      efExcel, efCSV: begin
        Separator := GridData.Connection.UnescapeString(editSeparator.Text);
        Encloser := GridData.Connection.UnescapeString(editEncloser.Text);
        Terminator := GridData.Connection.UnescapeString(editTerminator.Text);
        if chkIncludeColumnNames.Checked then begin
          Col := Grid.Header.Columns.GetFirstVisibleColumn(True);
          while Col > NoColumn do begin
            // Alter column name in header if data is not raw.
            ResultCol := Col - 1;
            if DoIncludeCol then begin
              Data := Grid.Header.Columns[Col].Text;
              if (GridData.DataType(ResultCol).Category in [dtcBinary, dtcSpatial]) and (not Mainform.actBlobAsText.Checked) then
                Data := 'HEX(' + Data + ')';
              // Add header item.
              if Header <> '' then
                Header := Header + Separator;
              Header := Header + Encloser + Data + Encloser;
            end;
            Col := Grid.Header.Columns.GetNextVisibleColumn(Col);
          end;
          Header := Header + Terminator;
        end;
      end;

      efXML: begin
        // Imitate mysqldump's XML style
        Header := '<?xml version="1.0" encoding="'+GetHTMLCharsetByEncoding(Encoding)+'"?>' + CRLF + CRLF;
        if chkIncludeQuery.Checked then
          Header := Header + '<resultset statement="'+HTMLSpecialChars(GridData.SQL)+'" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">' + CRLF
        else
          Header := Header + '<table_data name="'+HTMLSpecialChars(TableName)+'">' + CRLF;
      end;

      efLaTeX: begin
        Header := '\begin{tabular}';
        Separator := ' & ';
        Encloser := '';
        Terminator := '\\ '+CRLF;
        Header := Header + '{';
        Col := Grid.Header.Columns.GetFirstVisibleColumn(True);
        while Col > NoColumn do begin
          if DoIncludeCol then
            Header := Header + ' c ';
          Col := Grid.Header.Columns.GetNextVisibleColumn(Col);
        end;
        Header := Header + '}' + CRLF;
        if chkIncludeColumnNames.Checked then begin
          Col := Grid.Header.Columns.GetFirstVisibleColumn(True);
          while Col > NoColumn do begin
            if DoIncludeCol then
              Header := Header + FormatLatex(Grid.Header.Columns[Col].Text) + Separator;
            Col := Grid.Header.Columns.GetNextVisibleColumn(Col);
          end;
          Delete(Header, Length(Header)-Length(Separator)+1, Length(Separator));
          Header := Header + Terminator;
        end;
      end;

      efTextile, efJiraTextile: begin
        Separator := IfThen(CurrentExportFormat=efTextile, ' |_. ', ' || ');
        Encloser := '';
        Terminator := IfThen(CurrentExportFormat=efTextile, ' |', ' ||') + CRLF;
        if chkIncludeColumnNames.Checked then begin
          Header := TrimLeft(Separator);
          Col := Grid.Header.Columns.GetFirstVisibleColumn(True);
          while Col > NoColumn do begin
            if DoIncludeCol then
              Header := Header + Grid.Header.Columns[Col].Text  + Separator;
            Col := Grid.Header.Columns.GetNextVisibleColumn(Col);
          end;
          Delete(Header, Length(Header)-Length(Separator)+1, Length(Separator));
          Header := Header + Terminator;
        end;
        Separator := ' | ';
        Terminator := ' |' + CRLF;
      end;

      efPHPArray: begin
        if radioOutputFile.Checked then
          Header := '<?php'+CRLF+'$'+TableName+' = ['+CRLF
        else
          Header := '$'+TableName+' = ['+CRLF;
      end;

      efMarkDown: begin
        Separator := ' | ';
        Encloser := '';
        Terminator := CRLF;
        Header := Header + TableName + CRLF + '---' + CRLF;
        if chkIncludeQuery.Checked then
          Header := Header + '```sql' + CRLF + GridData.SQL + CRLF + '```' + CRLF;
        Header := Header + TrimLeft(Separator);
        Col := Grid.Header.Columns.GetFirstVisibleColumn(True);
        while Col > NoColumn do begin
          if DoIncludeCol then begin
            if chkIncludeColumnNames.Checked then
              Header := Header + Grid.Header.Columns[Col].Text + Separator
            else
              Header := Header + Separator
          end;
          Col := Grid.Header.Columns.GetNextVisibleColumn(Col);
        end;
        Header := Header + Terminator;
        // Write an extra line with dashes below the heading, otherwise the table won't parse
        Header := Header + TrimLeft(Separator);
        Col := Grid.Header.Columns.GetFirstVisibleColumn(True);
        while Col > NoColumn do begin
          ResultCol := Col - 1;
          if DoIncludeCol then begin
            Header := Header + '---';
            if GridData.DataType(ResultCol).Category in [dtcInteger, dtcReal] then
              Header := Header + ':';
            Header := Header + Separator;
          end;
          Col := Grid.Header.Columns.GetNextVisibleColumn(Col);
        end;
        Header := Header + Terminator;
      end;

      efJSON: begin
        // JavaScript Object Notation
        Header := '{' + sLineBreak;
        if chkIncludeQuery.Checked then
          Header := Header + #9 + '"query": '+FormatJson(GridData.SQL)+',' + sLineBreak
        else
          Header := Header + #9 + '"table": '+FormatJson(TableName)+',' + sLineBreak;
        Header := Header + #9 + '"rows":' + sLineBreak + #9 + '[';
      end;

    end;
    S.WriteString(Header);

    Node := GetNextNode(Grid, nil, SelectionOnly);
    while Assigned(Node) do begin
      // Update status once in a while.
      if (Node.Index+1) mod 100 = 0 then begin
        MainForm.ShowStatusMsg(f_('Exporting row %s of %s (%d%%, %s)',
          [FormatNumber(Node.Index+1),
          FormatNumber(NodeCount),
          Trunc((Node.Index+1) / NodeCount *100),
          FormatByteNumber(S.Size)]
          ));
        MainForm.ProgressStep;
      end;
      RowNum := Grid.GetNodeData(Node);
      GridData.RecNo := RowNum^;

      // Row preamble
      case CurrentExportFormat of
        efHTML: tmp := CodeIndent(4) + '<tr>' + sLineBreak;

        efXML: tmp := CodeIndent + '<row>' + sLineBreak;

        efSQLUpdate: begin
          tmp := '';
          tmp := tmp + 'UPDATE ' + GridData.Connection.QuoteIdent(Tablename) + ' SET ';
        end;

        efSQLInsert, efSQLInsertIgnore, efSQLReplace, efSQLDeleteInsert: begin
          tmp := '';
          if CurrentExportFormat = efSQLDeleteInsert then begin
            tmp := tmp + 'DELETE FROM ' + GridData.Connection.QuoteIdent(Tablename) + ' WHERE' + GridData.GetWhereClause + ';' + CRLF;
          end;

          if CurrentExportFormat in [efSQLInsert, efSQLDeleteInsert] then
            tmp := tmp + 'INSERT'
          else if CurrentExportFormat = efSQLInsertIgnore then
            tmp := tmp + 'INSERT IGNORE'
          else
            tmp := tmp + 'REPLACE';
          tmp := tmp + ' INTO '+GridData.Connection.QuoteIdent(Tablename);
          if chkIncludeColumnNames.Checked then begin
            tmp := tmp + ' (';
            Col := Grid.Header.Columns.GetFirstVisibleColumn(True);
            while Col > NoColumn do begin
              ResultCol := Col - 1;
              if DoIncludeCol and (not GridData.ColIsVirtual(ResultCol)) then
                tmp := tmp + GridData.Connection.QuoteIdent(Grid.Header.Columns[Col].Text)+', ';
              Col := Grid.Header.Columns.GetNextVisibleColumn(Col);
            end;
            Delete(tmp, Length(tmp)-1, 2);
            tmp := tmp + ')';
          end;
          tmp := tmp + ' VALUES (';
        end;

        efTextile, efJiraTextile: tmp := TrimLeft(Separator);

        efPHPArray: tmp := CodeIndent + '[' + sLineBreak;

        efMarkDown: tmp := '| ';

        efJSON: begin
          if chkIncludeColumnNames.Checked then
            tmp := sLineBreak + CodeIndent(2) + '{' + sLineBreak
          else
            tmp := sLineBreak + CodeIndent(2) + '[' + sLineBreak
        end;

        efJSONLines: begin
          if chkIncludeColumnNames.Checked then
            tmp := '{'
          else
            tmp := '[';
        end

        else tmp := '';
      end;

      // Row contents
      Col := Grid.Header.Columns.GetFirstVisibleColumn(True);
      while Col > NoColumn do begin
        ResultCol := Col - 1;
        if DoIncludeCol then begin
          if (GridData.DataType(ResultCol).Category in [dtcBinary, dtcSpatial])
            and (not Mainform.actBlobAsText.Checked) then begin
            Data := GridData.HexValue(ResultCol);
          end else begin
            Data := GridData.Col(ResultCol);
            HasNulls := False;
            RemoveNullChars(Data, HasNulls);
          end;

          // Keep formatted numeric values
          if (GridData.DataType(ResultCol).Category in [dtcInteger, dtcReal])
            and (CurrentExportFormat in [efExcel, efHTML, efMarkDown])
            then begin
            Data := FormatNumber(Data, False);
          end;

          // Remove linebreaks, see #474
          if chkRemoveLinebreaks.Checked then begin
            StripNewLines(Data);
          end;

          case CurrentExportFormat of
            efHTML: begin
              // Escape HTML control characters in data.
              Data := HTMLSpecialChars(Data);
              tmp := tmp + CodeIndent(5) + '<td class="col' + IntToStr(Col) + '">' + Data + '</td>' + sLineBreak;
            end;

            efExcel, efCSV: begin
              if GridData.IsNull(ResultCol) then
                Data := editNull.Text
              else begin
                Data := FormatCsv(Data, Encloser, GridData.DataType(ResultCol), CurrentExportFormat);
                Data := Encloser + Data + Encloser;
              end;
              tmp := tmp + Data + Separator;
            end;

            efLaTeX: begin
              Data := FormatLatex(Data);
              if (not GridData.IsNull(ResultCol)) and (GridData.DataType(ResultCol).Category in [dtcInteger, dtcReal]) then
                // Special encloser for numeric values, see https://www.heidisql.com/forum.php?t=36530
                Data := '$' + Data + '$';
              tmp := tmp + Data + Separator;
            end;

            efTextile, efJiraTextile: begin
              tmp := tmp + Data + Separator;
            end;

            efMarkDown: begin
              if GridData.IsNull(ResultCol) then
                Data := editNull.Text;
              tmp := tmp + Data + Separator;
            end;

            efXML: begin
              // Print cell start tag.
              tmp := tmp + CodeIndent(2) + '<field';
              if chkIncludeColumnNames.Checked then
                tmp := tmp + ' name="' + HTMLSpecialChars(Grid.Header.Columns[Col].Text) + '"';
              if GridData.IsNull(ResultCol) then
                tmp := tmp + ' xsi:nil="true" />' + CRLF
              else begin
                if (GridData.DataType(ResultCol).Category in [dtcBinary, dtcSpatial]) and (not Mainform.actBlobAsText.Checked) then
                  tmp := tmp + ' format="hex"';
                tmp := tmp + '>' + HTMLSpecialChars(Data) + '</field>' + CRLF;
              end;
            end;

            efSQLInsert, efSQLInsertIgnore, efSQLReplace, efSQLDeleteInsert, efSQLUpdate: begin
              if GridData.ColIsVirtual(ResultCol) then
                Data := ''
              else if GridData.IsNull(ResultCol) then
                Data := 'NULL'
              else if (GridData.DataType(ResultCol).Index = dbdtBit) and GridData.Connection.Parameters.IsAnyMySQL then
                Data := GridData.Connection.EscapeString(Data, GridData.DataType(ResultCol))
              else if (GridData.DataType(ResultCol).Category in [dtcText, dtcTemporal, dtcOther])
                or ((GridData.DataType(ResultCol).Category in [dtcBinary, dtcSpatial]) and Mainform.actBlobAsText.Checked)
                then
                Data := GridData.Connection.EscapeString(Data)
              else if Data = '' then
                Data := GridData.Connection.EscapeString(Data);
              if not Data.IsEmpty then begin
                if CurrentExportFormat = efSQLUpdate then
                  tmp := tmp + GridData.Connection.QuoteIdent(Grid.Header.Columns[Col].Text) + '=';
                tmp := tmp + Data + ', ';
              end;
            end;

            efPHPArray: begin
              if GridData.IsNull(ResultCol) then
                Data := 'null'
              else case GridData.DataType(ResultCol).Category of
                dtcInteger, dtcReal: begin
                  // Remove zeropadding to avoid octal => integer conversion in PHP
                  Data := FormatNumber(Data);
                  Data := UnformatNumber(Data);
                end;
                else
                  Data := FormatPhp(Data);
              end;

              if chkIncludeColumnNames.Checked then
                tmp := tmp + CodeIndent(2) + FormatPhp(Grid.Header.Columns[Col].Text) + ' => ' + Data + ',' + sLineBreak
              else
                tmp := tmp + CodeIndent(2) + Data + ',' + sLineBreak;
            end;

            efJSON: begin
              tmp := tmp + CodeIndent(3);
              if chkIncludeColumnNames.Checked then
                tmp := tmp + FormatJson(Grid.Header.Columns[Col].Text) + ': ';
              if GridData.IsNull(ResultCol) then
                tmp := tmp + 'null,' +CRLF
              else begin
                case GridData.DataType(ResultCol).Category of
                  dtcInteger, dtcReal:
                    tmp := tmp + Data;
                  else
                    tmp := tmp + FormatJson(Data)
                end;
                tmp := tmp + ',' + CRLF;
              end;
            end;

            efJSONLines: begin
              if chkIncludeColumnNames.Checked then
                tmp := tmp + FormatJson(Grid.Header.Columns[Col].Text) + ': ';
              if GridData.IsNull(ResultCol) then
                tmp := tmp + 'null, '
              else begin
                case GridData.DataType(ResultCol).Category of
                  dtcInteger, dtcReal:
                    tmp := tmp + Data;
                  else
                    tmp := tmp + FormatJson(Data)
                end;
                tmp := tmp + ', ';
              end;
            end;

          end;
        end;

        Col := Grid.Header.Columns.GetNextVisibleColumn(Col);
      end;

      // Row epilogue
      case CurrentExportFormat of
        efHTML:
          tmp := tmp + CodeIndent(4) + '</tr>' + sLineBreak;
        efExcel, efCSV, efLaTeX, efTextile, efJiraTextile: begin
          Delete(tmp, Length(tmp)-Length(Separator)+1, Length(Separator));
          tmp := tmp + Terminator;
        end;
        efXML:
          tmp := tmp + CodeIndent + '</row>' + sLineBreak;
        efSQLInsert, efSQLInsertIgnore, efSQLReplace, efSQLDeleteInsert: begin
          Delete(tmp, Length(tmp)-1, 2);
          tmp := tmp + ');' + CRLF;
        end;
        efSQLUpdate : begin
          Delete(tmp, length(tmp)-1,2);
          tmp := tmp + ' WHERE' + GridData.GetWhereClause + ';' + sLineBreak;
        end;
        efPHPArray:
          tmp := tmp + CodeIndent + '],' + sLineBreak;
        efMarkDown:
          tmp := tmp + Terminator;
        efJSON: begin
          Delete(tmp, length(tmp)-2,2);
          if chkIncludeColumnNames.Checked then
            tmp := tmp + CodeIndent(2) + '},'
          else
            tmp := tmp + CodeIndent(2) + '],';
        end;
        efJSONLines: begin
          Delete(tmp, length(tmp)-1,2);
          if chkIncludeColumnNames.Checked then
            tmp := tmp + '}' + #10
          else
            tmp := tmp + ']' + #10;
        end;
      end;
      S.WriteString(tmp);

      Node := GetNextNode(Grid, Node, SelectionOnly);
    end;

    // Footer
    case CurrentExportFormat of
      efHTML: begin
        tmp :=
          CodeIndent(3) + '</tbody>' + sLineBreak +
          CodeIndent(2) + '</table>' + sLineBreak + sLineBreak +
          CodeIndent(2) + '<p>' + sLineBreak +
          CodeIndent(3) + '<em>generated ' + DateToStr(now) + ' ' + TimeToStr(now) +
          CodeIndent(3) + 'by <a href="'+APPDOMAIN+'">' + APPNAME + ' ' + Mainform.AppVersion + '</a></em>' + sLineBreak +
          CodeIndent(2) + '</p>' + sLineBreak + sLineBreak +
          CodeIndent + '</body>' + sLineBreak +
          '</html>' + sLineBreak;
      end;
      efXML: begin
        if chkIncludeQuery.Checked then
          tmp := '</resultset>' + CRLF
        else
          tmp := '</table_data>' + CRLF;
      end;
      efLaTeX:
        tmp := '\end{tabular}' + CRLF;
      efPHPArray: begin
        tmp := '];' + CRLF;
      end;
      efJSON: begin
        S.Size := S.Size - 1;
        tmp := sLineBreak + CodeIndent + ']' + sLineBreak + '}';
      end;
      else
        tmp := '';
    end;
    S.WriteString(tmp);

    if radioOutputCopyToClipboard.Checked then begin
      HTML := nil;
      // SynEdit's exporter is slow on large strings, see issue #2903
      if S.Size < 100*SIZE_KB then begin
        case CurrentExportFormat of
          efSQLInsert, efSQLInsertIgnore, efSQLReplace, efSQLDeleteInsert: begin
            Exporter := TSynExporterHTML.Create(Self);
            Exporter.Highlighter := MainForm.SynSQLSynUsed;
            Exporter.ExportAll(Explode(CRLF, S.DataString));
            HTML := TMemoryStream.Create;
            Exporter.SaveToStream(HTML);
            Exporter.Free;
          end;
          efHTML: HTML := S;
        end;
      end;
      StreamToClipboard(S, HTML);
    end else begin
      try
        S.SaveToFile(Filename);
        if chkOpenFile.Checked then
          OpenDocument(editFilename.Text);
      except
        on E:EFCreateError do begin
          // Keep form open if file cannot be created
          ModalResult := mrNone;
          MainForm.SetProgressState(pbsError);
          ErrorDialog(E.Message);
        end;
      end;
    end;
    Mainform.ShowStatusMsg(_('Freeing data...'));
    FreeAndNil(S);

  except
    // Whole export code wrapped here
    on E:EDbError do begin
      Screen.Cursor := crDefault;
      ErrorDialog(E.Message);
    end
    else
      raise;
  end;

  Mainform.DisableProgress;
  Mainform.ShowStatusMsg;
  Screen.Cursor := crDefault;
end;


end.
