mirror of
https://github.com/doublecmd/doublecmd.git
synced 2026-06-21 09:58:13 +00:00
464 lines
11 KiB
ObjectPascal
464 lines
11 KiB
ObjectPascal
{
|
|
Double Commander
|
|
-------------------------------------------------------------------------
|
|
Modified version of standard Masks unit
|
|
|
|
Copyright (C) 2010-2021 Alexander Koblov (alexx2000@mail.ru)
|
|
|
|
This file is based on masks.pas from the Lazarus Component Library (LCL)
|
|
|
|
See the file COPYING.modifiedLGPL.txt, included in this distribution,
|
|
for details about the copyright.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
|
}
|
|
|
|
unit uMasks;
|
|
|
|
{$mode objfpc}{$H+}
|
|
|
|
interface
|
|
|
|
uses
|
|
Classes, SysUtils, Contnrs;
|
|
|
|
type
|
|
TMaskCharType = (mcChar, mcAnyChar, mcAnyText);
|
|
TMaskOption = (moCaseSensitive, moIgnoreAccents, moWindowsMask, moPinyin);
|
|
TMaskOptions = set of TMaskOption;
|
|
|
|
TMaskChar = record
|
|
case CharType: TMaskCharType of
|
|
mcChar: (CharValue: WideChar);
|
|
mcAnyChar, mcAnyText: ();
|
|
end;
|
|
|
|
TMaskString = record
|
|
MinLength: Integer;
|
|
MaxLength: Integer;
|
|
Chars: Array of TMaskChar;
|
|
end;
|
|
|
|
{ TMask }
|
|
TMask = class
|
|
private
|
|
FTemplate:string;
|
|
FMask: TMaskString;
|
|
FUsePinyin: Boolean;
|
|
FCaseSensitive: Boolean;
|
|
fIgnoreAccents: Boolean;
|
|
fWindowsInterpretation: boolean;
|
|
|
|
procedure SetCaseSence(ACaseSence:boolean);
|
|
procedure SetTemplate(AValue: String);
|
|
procedure Update;
|
|
public
|
|
constructor Create(const AValue: string; const AOptions: TMaskOptions = []);
|
|
function Matches(const AFileName: string): boolean;
|
|
function LegacyMatches(const AFileName: string): boolean;
|
|
function WindowsMatches(const AFileName: string): boolean;
|
|
property CaseSensitive:boolean read FCaseSensitive write SetCaseSence;
|
|
property Template:string read FTemplate write SetTemplate;
|
|
end;
|
|
|
|
{ TParseStringList }
|
|
TParseStringList = class(TStringList)
|
|
public
|
|
constructor Create(const AText, ASeparators: String);
|
|
end;
|
|
|
|
{ TMaskList }
|
|
TMaskList = class
|
|
private
|
|
FMasks: TObjectList;
|
|
function GetCount: Integer;
|
|
function GetItem(Index: Integer): TMask;
|
|
public
|
|
constructor Create(const AValue: string; ASeparatorCharset: string = ';'; const AOptions: TMaskOptions = []);
|
|
destructor Destroy; override;
|
|
|
|
function Matches(const AFileName: String): Boolean;
|
|
|
|
property Count: Integer read GetCount;
|
|
property Items[Index: Integer]: TMask read GetItem;
|
|
end;
|
|
|
|
function MatchesMask(const FileName, Mask: String; const AOptions: TMaskOptions = []): Boolean;
|
|
function MatchesMaskList(const FileName, Mask: string; ASeparatorCharset: string = ';'; const AOptions: TMaskOptions = []): boolean;
|
|
|
|
implementation
|
|
|
|
uses
|
|
//Lazarus, Free-Pascal, etc.
|
|
LazUTF8,
|
|
|
|
//DC
|
|
DCConvertEncoding, uPinyin, uAccentsUtils;
|
|
|
|
{ MatchesMask }
|
|
function MatchesMask(const FileName, Mask: String; const AOptions: TMaskOptions): Boolean;
|
|
var
|
|
AMask: TMask;
|
|
begin
|
|
if Mask <> '' then
|
|
begin
|
|
AMask := TMask.Create(Mask, AOptions);
|
|
try
|
|
Result := AMask.Matches(FileName);
|
|
finally
|
|
AMask.Free;
|
|
end;
|
|
end
|
|
else
|
|
Result := False;
|
|
end;
|
|
|
|
{ MatchesMaskList }
|
|
function MatchesMaskList(const FileName, Mask: string; ASeparatorCharset: string; const AOptions: TMaskOptions): boolean;
|
|
var
|
|
AMaskList: TMaskList;
|
|
begin
|
|
if Mask <> '' then
|
|
begin
|
|
AMaskList := TMaskList.Create(Mask, ASeparatorCharset, AOptions);
|
|
try
|
|
Result := AMaskList.Matches(FileName);
|
|
finally
|
|
AMaskList.Free;
|
|
end;
|
|
end
|
|
else
|
|
Result := False;
|
|
end;
|
|
|
|
|
|
{ TMask }
|
|
|
|
{ TMask.Create }
|
|
constructor TMask.Create(const AValue: string; const AOptions: TMaskOptions);
|
|
begin
|
|
FTemplate:= AValue;
|
|
FUsePinyin:= moPinyin in AOptions;
|
|
FCaseSensitive := moCaseSensitive in AOptions;
|
|
fIgnoreAccents := moIgnoreAccents in AOptions;
|
|
fWindowsInterpretation := moWindowsMask in AOptions;
|
|
if FIgnoreAccents then FTemplate := NormalizeAccentedChar(FTemplate); //Let's set the mask early in straight letters if match attempt has to be with accent and ligature removed.
|
|
if not FCaseSensitive then FTemplate := UTF8LowerCase(FTemplate); //Let's set the mask early in lowercase if match attempt has to be case insensitive.
|
|
Update;
|
|
end;
|
|
|
|
{ TMask.SetCaseSence }
|
|
procedure TMask.SetCaseSence(ACaseSence:boolean);
|
|
begin
|
|
FCaseSensitive:=ACaseSence;
|
|
Update;
|
|
end;
|
|
|
|
{ TMask.SetTemplate }
|
|
procedure TMask.SetTemplate(AValue: String);
|
|
begin
|
|
FTemplate:=AValue;
|
|
Update;
|
|
end;
|
|
|
|
{ TMask.Update }
|
|
procedure TMask.Update;
|
|
var
|
|
I: Integer;
|
|
S: UnicodeString;
|
|
SkipAnyText: Boolean;
|
|
AValue:string;
|
|
|
|
procedure AddAnyText;
|
|
begin
|
|
if SkipAnyText then
|
|
begin
|
|
Inc(I);
|
|
Exit;
|
|
end;
|
|
|
|
SetLength(FMask.Chars, Length(FMask.Chars) + 1);
|
|
FMask.Chars[High(FMask.Chars)].CharType := mcAnyText;
|
|
|
|
FMask.MaxLength := MaxInt;
|
|
SkipAnyText := True;
|
|
Inc(I);
|
|
end;
|
|
|
|
procedure AddAnyChar;
|
|
begin
|
|
SkipAnyText := False;
|
|
|
|
SetLength(FMask.Chars, Length(FMask.Chars) + 1);
|
|
FMask.Chars[High(FMask.Chars)].CharType := mcAnyChar;
|
|
|
|
Inc(FMask.MinLength);
|
|
if FMask.MaxLength < MaxInt then Inc(FMask.MaxLength);
|
|
|
|
Inc(I);
|
|
end;
|
|
|
|
procedure AddChar;
|
|
begin
|
|
SkipAnyText := False;
|
|
|
|
SetLength(FMask.Chars, Length(FMask.Chars) + 1);
|
|
with FMask.Chars[High(FMask.Chars)] do
|
|
begin
|
|
CharType := mcChar;
|
|
CharValue := S[I];
|
|
end;
|
|
|
|
Inc(FMask.MinLength);
|
|
if FMask.MaxLength < MaxInt then Inc(FMask.MaxLength);
|
|
|
|
Inc(I);
|
|
end;
|
|
|
|
begin
|
|
AValue:=FTemplate;
|
|
|
|
SetLength(FMask.Chars, 0);
|
|
FMask.MinLength := 0;
|
|
FMask.MaxLength := 0;
|
|
SkipAnyText := False;
|
|
S := CeUtf8ToUtf16(AValue);
|
|
|
|
I := 1;
|
|
while I <= Length(S) do
|
|
begin
|
|
case S[I] of
|
|
'*': AddAnyText;
|
|
'?': AddAnyChar;
|
|
else AddChar;
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
{ TMask.Matches }
|
|
function TMask.Matches(const AFileName: string): boolean;
|
|
var
|
|
sFilename: string;
|
|
begin
|
|
//Let's set the AFileName in straight letters if match attempt has to be with accent and ligature removed.
|
|
if FIgnoreAccents then
|
|
sFilename := NormalizeAccentedChar(AFileName)
|
|
else
|
|
sFilename := AFileName;
|
|
|
|
//Let's set our AFileName is lowercase early if not case-sensitive
|
|
if not FCaseSensitive then
|
|
sFilename := UTF8LowerCase(sFilename);
|
|
|
|
if not fWindowsInterpretation then
|
|
Result := LegacyMatches(sFileName)
|
|
else
|
|
Result := WindowsMatches(sFileName);
|
|
end;
|
|
|
|
{ TMask.LegacyMatches }
|
|
function TMask.LegacyMatches(const AFileName: string): boolean;
|
|
var
|
|
L: Integer;
|
|
S: UnicodeString;
|
|
|
|
function MatchToEnd(MaskIndex, CharIndex: Integer): Boolean;
|
|
var
|
|
I, J: Integer;
|
|
begin
|
|
Result := False;
|
|
|
|
for I := MaskIndex to High(FMask.Chars) do
|
|
begin
|
|
case FMask.Chars[I].CharType of
|
|
mcChar:
|
|
begin
|
|
if CharIndex > L then Exit;
|
|
//DCDebug('Match ' + S[CharIndex] + '<?>' + FMask.Chars[I].CharValue);
|
|
if FUsePinyin then
|
|
begin
|
|
if not PinyinMatch(S[CharIndex], FMask.Chars[I].CharValue) then exit;
|
|
end
|
|
else
|
|
begin
|
|
if S[CharIndex] <> FMask.Chars[I].CharValue then Exit;
|
|
end;
|
|
Inc(CharIndex);
|
|
end;
|
|
mcAnyChar:
|
|
begin
|
|
if CharIndex > L then Exit;
|
|
Inc(CharIndex);
|
|
end;
|
|
mcAnyText:
|
|
begin
|
|
if I = High(FMask.Chars) then
|
|
begin
|
|
Result := True;
|
|
Exit;
|
|
end;
|
|
|
|
for J := CharIndex to L do
|
|
if MatchToEnd(I + 1, J) then
|
|
begin
|
|
Result := True;
|
|
Exit;
|
|
end;
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
Result := CharIndex > L;
|
|
end;
|
|
|
|
begin
|
|
Result := False;
|
|
S := CeUtf8ToUtf16(AFileName);
|
|
L := Length(S);
|
|
if L = 0 then
|
|
begin
|
|
if FMask.MinLength = 0 then Result := True;
|
|
Exit;
|
|
end;
|
|
|
|
if (L < FMask.MinLength) or (L > FMask.MaxLength) then Exit;
|
|
|
|
Result := MatchToEnd(0, 1);
|
|
end;
|
|
|
|
{ TMask.WindowsMatches }
|
|
// treat initial mask differently for special cases:
|
|
// foo*.* -> foo*
|
|
// foo*. -> match foo*, but muts not have an extension
|
|
// *. -> any file without extension ( .foo is a filename without extension according to Windows)
|
|
// foo. matches only foo but not foo.txt
|
|
// foo.* -> match either foo or foo.*
|
|
function TMask.WindowsMatches(const AFileName: string): boolean;
|
|
var
|
|
Ext, sInitialTemplate: string;
|
|
sInitialMask: UnicodeString;
|
|
|
|
begin
|
|
sInitialMask := CeUtf8ToUtf16(FTemplate);
|
|
|
|
if (Length(sInitialMask) > 2) and (RightStr(sInitialMask, 3) = '*.*') then // foo*.*
|
|
begin
|
|
sInitialTemplate := FTemplate; //Preserve initial state of FTemplate
|
|
FTemplate := Copy(sInitialMask, 1, Length(sInitialMask) - 2);
|
|
Update;
|
|
Result := LegacyMatches(AFileName);
|
|
FTemplate := sInitialTemplate; //Restore initial state of FTemplate
|
|
Update;
|
|
end
|
|
else if (Length(sInitialMask) > 1) and (RightStr(sInitialMask, 1) = '.') then //foo*. or *. or foo.
|
|
begin
|
|
//if AFileName has an extension then Result is False, otherwise see if it LegacyMatches foo*/foo
|
|
//a filename like .foo under Windows is considered to be a file without an extension
|
|
Ext := ExtractFileExt(AFileName);
|
|
if (Ext = '') or (Ext = AFileName) then
|
|
begin
|
|
sInitialTemplate := FTemplate; //Preserve initial state of FTemplate
|
|
FTemplate := Copy(sInitialMask, 1, Length(sInitialMask) - 1);
|
|
Update;
|
|
Result := LegacyMatches(AFileName);
|
|
FTemplate := sInitialTemplate; //Restore initial state of FTemplate
|
|
Update;
|
|
end
|
|
else
|
|
begin
|
|
Result := False;
|
|
end;
|
|
end
|
|
else if (Length(sInitialMask) > 2) and (RightStr(sInitialMask, 2) = '.*') then //foo.* (but not '.*')
|
|
begin
|
|
//First see if we have 'foo'
|
|
Result := (AFileName = Copy(sInitialMask, 1, Length(sInitialMask) - 2));
|
|
if not Result then Result := LegacyMatches(AFileName);
|
|
end
|
|
else
|
|
begin
|
|
Result := LegacyMatches(AFileName); //all other cases just call LegacyMatches()
|
|
end;
|
|
end;
|
|
|
|
{ TParseStringList }
|
|
{ TParseStringList.Create }
|
|
constructor TParseStringList.Create(const AText, ASeparators: String);
|
|
var
|
|
I, S: Integer;
|
|
begin
|
|
inherited Create;
|
|
|
|
S := 1;
|
|
for I := 1 to Length(AText) do
|
|
begin
|
|
if Pos(AText[I], ASeparators) > 0 then
|
|
begin
|
|
if I > S then Add(Copy(AText, S, I - S));
|
|
S := I + 1;
|
|
end;
|
|
end;
|
|
|
|
if Length(AText) >= S then Add(Copy(AText, S, Length(AText) - S + 1));
|
|
end;
|
|
|
|
|
|
{ TMaskList }
|
|
|
|
function TMaskList.GetItem(Index: Integer): TMask;
|
|
begin
|
|
Result := TMask(FMasks.Items[Index]);
|
|
end;
|
|
|
|
{ TMaskList.GetCount }
|
|
function TMaskList.GetCount: Integer;
|
|
begin
|
|
Result := FMasks.Count;
|
|
end;
|
|
|
|
{ TMaskList.Create }
|
|
constructor TMaskList.Create(const AValue: string; ASeparatorCharset: string; const AOptions: TMaskOptions);
|
|
var
|
|
I: Integer;
|
|
S: TParseStringList;
|
|
begin
|
|
FMasks := TObjectList.Create(True);
|
|
if AValue = '' then exit;
|
|
|
|
S := TParseStringList.Create(AValue, ASeparatorCharset);
|
|
try
|
|
for I := 0 to S.Count - 1 do
|
|
FMasks.Add(TMask.Create(S[I], AOptions));
|
|
finally
|
|
S.Free;
|
|
end;
|
|
end;
|
|
|
|
{ TMaskList.Destroy }
|
|
destructor TMaskList.Destroy;
|
|
begin
|
|
FMasks.Free;
|
|
|
|
inherited Destroy;
|
|
end;
|
|
|
|
{ TMaskList.Matches }
|
|
function TMaskList.Matches(const AFileName: String): Boolean;
|
|
var
|
|
I: integer;
|
|
begin
|
|
Result := False;
|
|
|
|
for I := 0 to FMasks.Count - 1 do
|
|
begin
|
|
if TMask(FMasks.Items[I]).Matches(AFileName) then
|
|
begin
|
|
Result := True;
|
|
Exit;
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
end.
|