unit atsynedit_form_complete_html;

{$mode objfpc}{$H+}

interface

uses
  Classes, SysUtils, Graphics,
  ATSynEdit,
  ATSynEdit_Carets,
  RegExpr,
  Dialogs;

//it needs file html_list.ini from SynWrite distro
procedure DoEditorCompletionHtml(AEdit: TATSynEdit;
  const AFilenameHtmlList: string);

type
  TCompleteHtmlMode = (
    acpModeNone,
    acpModeTags,
    acpModeTagsClose,
    acpModeAttrs,
    acpModeVals
    );

//detect tag and its attribute at caret pos
procedure EditorGetHtmlTag(Ed: TATSynedit; out STag, SAttr: string;
  out AMode: TCompleteHtmlMode);
function EditorHasCssAtCaret(Ed: TATSynEdit): boolean;


implementation

uses
  ATStringProc,
  ATSynEdit_form_complete;

type
  { TAcp }

  TAcp = class
  private
    List: TStringlist;
    procedure DoOnGetCompleteProp(Sender: TObject; out AText, ASuffix: string;
      out ACharsLeft, ACharsRight: integer);
  public
    Ed: TATSynEdit;
    constructor Create; virtual;
    destructor Destroy; override;
  end;

var
  Acp: TAcp = nil;

function SFindRegex(const SText, SRegex: string; NGroup: integer): string;
var
  R: TRegExpr;
begin
  Result:= '';
  R:= TRegExpr.Create;
  try
    R.ModifierS:= false;
    R.ModifierM:= true;
    R.ModifierI:= true;

    R.Expression:= SRegex;
    R.InputString:= SText;

    if R.ExecPos(1) then
      Result:= Copy(SText, R.MatchPos[NGroup], R.MatchLen[NGroup]);
  finally
    R.Free;
  end;
end;

procedure EditorGetHtmlTag(Ed: TATSynedit; out STag, SAttr: string; out AMode: TCompleteHtmlMode);
const
  //regex to catch tag name at line start
  cRegexTagPart = '^\w+\b';
  cRegexTagOnly = '^\w*$';
  cRegexTagClose = '^/\w*$';
  //character class for all chars inside quotes
  cRegexChars = '[\s\w,\.:;\-\+\*\?=\(\)\[\]\{\}/\\\|~`\^\$&%\#@!]';
  //regex to catch attrib name, followed by "=" and not-closed quote, only at line end
  cRegexAttr = '\b([\w\-]+)\s*\=\s*([''"]' + cRegexChars + '*)?$';
  //regex group
  cGroupTagPart = 0;
  cGroupTagOnly = 0;
  cGroupTagClose = 0;
  cGroupAttr = 1;
var
  Caret: TATCaretItem;
  S: atString;
  N: integer;
begin
  STag:= '';
  SAttr:= '';
  AMode:= acpModeNone;

  //str before caret
  Caret:= Ed.Carets[0];
  S:= Ed.Strings.Lines[Caret.PosY];
  S:= Copy(S, 1, Caret.PosX);
  if S='' then Exit;

  //cut string before last "<" or ">" char
  N:= Length(S);
  while (N>0) and (S[N]<>'<') and (S[N]<>'>') do Dec(N);
  if N=0 then Exit;
  Delete(S, 1, N);

  STag:= SFindRegex(S, cRegexTagClose, cGroupTagClose);
  if STag<>'' then
    begin AMode:= acpModeTagsClose; exit end;

  STag:= SFindRegex(S, cRegexTagOnly, cGroupTagOnly);
  if STag<>'' then
    begin AMode:= acpModeTags; exit end;

  STag:= SFindRegex(S, cRegexTagPart, cGroupTagPart);
  if STag<>'' then
  begin
    SAttr:= SFindRegex(S, cRegexAttr, cGroupAttr);
    if SAttr<>'' then
      AMode:= acpModeVals
    else
      AMode:= acpModeAttrs;
  end
  else
    AMode:= acpModeTags;
end;

function EditorHasCssAtCaret(Ed: TATSynEdit): boolean;
var
  STag, SAttr: string;
  Mode: TCompleteHtmlMode;
begin
  EditorGetHtmlTag(Ed, STag, SAttr, Mode);
  Result:= (Mode=acpModeVals) and (LowerCase(SAttr)='style');
end;


procedure TAcp.DoOnGetCompleteProp(Sender: TObject; out AText, ASuffix: string;
  out ACharsLeft, ACharsRight: integer);
const
  cWordChars = '-';
var
  mode: TCompleteHtmlMode;
  s_word: atString;
  s_tag, s_attr, s_item, s_subitem, s_value: string;
  i: integer;
  ok: boolean;
begin
  AText:= '';
  ASuffix:= '';
  ACharsLeft:= 0;
  ACharsRight:= 0;

  EditorGetHtmlTag(Ed, s_tag, s_attr, mode);
  EditorGetCurrentWord(Ed, cWordChars, s_word, ACharsLeft, ACharsRight);

  case mode of
    acpModeTags,
    acpModeTagsClose:
      begin
        if mode=acpModeTagsClose then
          ASuffix:= '>'
        else
          ASuffix:= ' ';

        for i:= 0 to List.Count-1 do
        begin
          s_item:= List.Names[i];

          //filter items
          if s_word<>'' then
          begin
            ok:= SBeginsWith(UpperCase(s_item), UpperCase(s_word));
            if not ok then Continue;
          end;
          AText:= AText+'tag|'+s_item+#13;
        end;
      end;

    acpModeAttrs:
      begin
        ASuffix:='=';
        s_item:= List.Values[s_tag];
        if s_item='' then exit;
        repeat
          s_subitem:= SGetItem(s_item, '|');
          if s_subitem='' then Break;
          s_subitem:= SGetItem(s_subitem, '<');

          //filter items
          if s_word<>'' then
          begin
            ok:= SBeginsWith(UpperCase(s_subitem), UpperCase(s_word));
            if not ok then Continue;
          end;
          AText:= AText+s_tag+' attrib|'+s_subitem+#13;
        until false;
      end;

    acpModeVals:
      begin
        ASuffix:=' ';
        s_item:= List.Values[s_tag];
        if s_item='' then exit;
        repeat
          s_subitem:= SGetItem(s_item, '|');
          if s_subitem='' then Break;
          if SGetItem(s_subitem, '<')<>s_attr then Continue;
          repeat
            s_value:= SGetItem(s_subitem, '?');
            if s_value='' then Break;
            AText:= AText+s_attr+' value|"'+s_value+'"'+#13;
          until false;
        until false;
      end;
  end;
end;

constructor TAcp.Create;
begin
  inherited;
  List:= TStringlist.create;
end;

destructor TAcp.Destroy;
begin
  FreeAndNil(List);
  inherited;
end;

procedure DoEditorCompletionHtml(AEdit: TATSynEdit;
  const AFilenameHtmlList: string);
begin
  Acp.Ed:= AEdit;

  //load file only once
  if Acp.List.Count=0 then
  begin
    if not FileExists(AFilenameHtmlList) then exit;
    Acp.List.LoadFromFile(AFilenameHtmlList);
  end;

  DoEditorCompletionListbox(AEdit, @Acp.DoOnGetCompleteProp);
end;

initialization
  Acp:= TAcp.Create;

  cCompleteFontStyles[0]:= [];
  cCompleteColorFont[0]:= clPurple;
  cCompleteColorFont[1]:= clBlack;

finalization
  FreeAndNil(Acp);

end.

