unit ASDoc;
interface
uses
Windows, SysUtils, Types, Classes, UITypes, Graphics, Controls, TextEditor,
Generics.Defaults, Generics.Collections, StdCtrls, ExtCtrls, ClientDefs,
ASObjects, StrUtils, ASStrFcns;
type
EDocException = class(EClientException);
TDocMode = (dmEdit, dmView);
TTopicInfo = record
Name: string;
Metadata: string;
end;
TASDoc = record
strict private
class var FPath: string;
class var FFullTextIndex: TArray<TPair<string, string>>;
class constructor ClassCreate;
class function GetMetadata: string; static;
public
const Version = '1.0';
class procedure PrepareViewer(AEditor: TTextEditor); static;
class function Parse(const ASource: TArray<string>;
ALinks: THyperlinks; AObjects: TObjectDictionary<Integer, TAlgosimObject>):
TTextFile; static;
class procedure SaveToFile(const ATopicName: string; AEditor: TTextEditor;
const AFileName: TFileName); static;
class procedure LoadFromFile(AEditor: TTextEditor;
const AFileName: TFileName; out ATopicInfo: TTopicInfo;
ADocMode: TDocMode); static;
class function GetTopicName(const AFileName: TFileName): string; static;
class function GetDocFileName(const ATopic: string;
ARaiseOnNotFound: Boolean): string; static;
class function GetDocFileNameForWeb(const ATopic: string;
ARaiseOnNotFound: Boolean): string; static;
class function GetTopics: TArray<string>; static;
class function Search(const ASearchText: string;
ASearchOptions: TStringSearchOptions = [ssoIgnoreCase]): TArray<string>; static;
class procedure CompileFullTextIndex; static;
class procedure CreateHypertextVersion; overload; static;
class procedure CreateHypertextVersion(const ATopic: string); overload; static;
class procedure CreateHypertextIndex; static;
class procedure CreateHypertextGotoPage; static;
class procedure RequireDoc; static;
class property Path: string read FPath;
end;
implementation
uses
Math, DateUtils, IOUtils, Character, Zip, PngImage, Registry, UnicodeData,
ASNum, ASKernel, SndPlayer, Gallery, Forms, FrontEndProps, MainForm, ShellAPI,
Hash, Clipbrd, ASTable;
type
TString = record
strict private
const
AllocStep = 1024;
var
FData: string;
FActualLength: Integer;
public
procedure Append(C: Char); overload;
procedure Append(const S: string); overload;
procedure TrimExcess;
constructor Create(ACapacity: Integer);
class operator Explicit(const S: TString): string;
class operator Implicit(const S: string): TString;
end;
function UrlEncode(const S: string): string;
begin
var temp := TString.Create(2*S.Length);
for var i := 1 to S.Length do
case S[i] of
'A'..'Z', 'a'..'z', '0'..'9', '-', '_', '.', '~':
temp.Append(S[i]);
else
for var b in TEncoding.UTF8.GetBytes(S[i]) do
temp.Append('%' + b.ToHexString(2));
end;
Result := string(temp);
end;
class constructor TASDoc.ClassCreate;
begin
with TRegistry.Create do
try
RootKey := HKEY_CURRENT_USER;
if OpenKey('Software\Rejbrand\Algosim', False) and ValueExists('DocPath') then
FPath := ReadString('DocPath')
else
FPath := TPath.Combine(ExtractFilePath(Application.ExeName), 'Doc');
finally
Free;
end;
end;
function ProcessText(const AText: string): string;
var
i, c: Integer;
begin
SetLength(Result, AText.Length);
c := 0;
for i := 1 to AText.Length do
if not AText[i].IsInArray(['§', '‹', '›', '`', #13, #10, #9]) then
begin
Inc(c);
Result[c] := AText[i];
end
else if AText[i] = #10 then
begin
Inc(c);
Result[c] := #32;
end;
SetLength(Result, c);
end;
class procedure TASDoc.CompileFullTextIndex;
var
Files: TArray<string>;
fn, Text, Name: string;
Zip: TZipFile;
TextBytes, NameBytes, FullTextBytes: TBytes;
Lines: TList<string>;
S: string;
begin
Lines := TList<string>.Create;
try
Files := TDirectory.GetFiles(FPath, '*.asdoc');
for fn in Files do
begin
Zip := TZipFile.Create;
try
Zip.Open(fn, zmRead);
try
if Zip.IndexOf('text.asml') = -1 then
Continue;
Zip.Read('text.asml', TextBytes);
Text := TEncoding.UTF8.GetString(TextBytes);
if Zip.IndexOf('name.txt') <> -1 then
begin
Zip.Read('name.txt', NameBytes);
Name := TEncoding.UTF8.GetString(NameBytes);
end
else
Name := TPath.GetFileNameWithoutExtension(fn);
Text := ProcessText(Text);
Name := ProcessText(Name);
Lines.Add(Name + #9 + Text);
finally
Zip.Close;
end;
finally
Zip.Free;
end;
end;
S := string.Join(#13#10, Lines.ToArray);
FullTextBytes := TEncoding.UTF8.GetBytes(S);
finally
Lines.Free;
end;
Zip := TZipFile.Create;
try
Zip.Open(TPath.Combine(FPath, 'fulltext.ftx'), zmWrite);
try
Zip.Add(FullTextBytes, 'fulltext.txt');
finally
Zip.Close;
end;
finally
Zip.Free;
end;
end;
const
Template =
'<!DOCTYPE html>'#13#10 +
'<html lang="en">'#13#10 +
'<head>'#13#10 +
'<title>%TITLE% – Algosim documentation</title>'#13#10 +
'<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />'#13#10 +
'<meta name="author" content="%AUTHOR%" />'#13#10 +
'<link rel="stylesheet" type="text/css" href="asdoc.css" />'#13#10 +
'</head>'#13#10 +
''#13#10 +
'<body>'#13#10 +
'<header>'#13#10 +
'%HEADER%'#13#10 +
'</header>'#13#10 +
'<main>'#13#10 +
'%CONTENT%'#13#10 +
'</main>'#13#10 +
'<footer>'#13#10 +
'%FOOTER%'#13#10 +
'</footer>'#13#10 +
'</body>'#13#10 +
'</html>';
function HtmlEscape(const S: string): string; overload;
begin
Result :=
S
.Replace('&', '&')
.Replace('<', '<')
.Replace('>', '>')
end;
function HtmlEscape(const C: Char): string; overload;
begin
case C of
'<':
Result := '<';
'>':
Result := '>';
'&':
Result := '&';
else
Result := C;
end;
end;
function JsStrLitEscape(const S: string): string;
begin
Result :=
S
.Replace('\', '\\')
.Replace(#0, '\0')
.Replace('''', '\''')
.Replace('"', '\"')
.Replace(#13#10, '\n')
.Replace(#13, '\n')
.Replace(#10, '\n')
.Replace(#9, '\t')
.Replace('<', '\u003C')
.Replace('>', '\u003E')
.Replace('&', '\u0026')
end;
class procedure TASDoc.CreateHypertextGotoPage;
const
Script =
'function GotoTopic()'#13#10 +
'{'#13#10 +
' var TopicName = document.getElementById("topic-name").value;'#13#10 +
' var TopicNameLC = TopicName.toLowerCase();'#13#10 +
' if (TopicName in topics)'#13#10 +
' {'#13#10 +
' window.location.replace(topics[TopicName]);'#13#10 +
' return false;'#13#10 +
' }'#13#10 +
' for (var topic in topics)'#13#10 +
' {'#13#10 +
' if (topic.toLowerCase() == TopicNameLC)'#13#10 +
' {'#13#10 +
' window.location.replace(topics[topic]);'#13#10 +
' return false;'#13#10 +
' }'#13#10 +
' }'#13#10 +
' alert("Topic \"" + TopicName + "\" not found.");'#13#10 +
' return false;'#13#10 +
'}';
begin
var HypertextRoot := TPath.Combine(Path, 'html');
var HtmlFileName := TPath.Combine(HypertextRoot, 'docgoto.html');
var LHeader := '';
if TFile.Exists(TPath.Combine(Path, 'header.inc')) then
LHeader := TFile.ReadAllText(TPath.Combine(Path, 'header.inc'), TEncoding.UTF8);
var LFooter := '';
if TFile.Exists(TPath.Combine(Path, 'footer.inc')) then
LFooter := TFile.ReadAllText(TPath.Combine(Path, 'footer.inc'), TEncoding.UTF8);
var MainContents := TList<string>.Create;
try
var UrlDict := TDictionary<string, string>.Create;
try
MainContents.Add('<div style="margin-top: 4em; margin-bottom: 4em">');
MainContents.Add(Format('<h1 style="text-align: center;">%s</h1>', [HtmlEscape('Please enter a topic to display:')]));
MainContents.Add('<form onsubmit="return GotoTopic()" style="text-align: center">');
MainContents.Add(' <label for="topic-name">Topic:</label>');
MainContents.Add(' <input list="topics" id="topic-name" name="topic-name" autofocus="" autocomplete="off" required="" />');
MainContents.Add(' <button>Go</button>');
MainContents.Add(' <datalist id="topics">');
for var Topic in GetTopics do
begin
if Topic.IsEmpty then
Continue;
var LinkURL := UrlEncode(TPath.GetFileNameWithoutExtension(TASDoc.GetDocFileNameForWeb(Topic, True))) + '.html';
MainContents.Add(Format(' <option>%s</option>', [HtmlEscape(Topic)]));
UrlDict.Add(Topic, LinkURL);
end;
MainContents.Add(' </datalist>');
MainContents.Add('</form>');
MainContents.Add('</div>');
MainContents.Add('<script language="JavaScript">');
MainContents.Add('<!--');
MainContents.Add('var topics = {');
for var p in UrlDict do
MainContents.Add(Format('"%s": "%s",', [JsStrLitEscape(p.Key), p.Value]));
MainContents.Add('"Home": "Algosim.html",');
MainContents.Add('"Documentation index": "docindex.html"');
MainContents.Add('};');
MainContents.Add(Script);
MainContents.Add('//-->');
MainContents.Add('</script>');
var LNow := TTimeZone.Local.ToUniversalTime(Now);
var PrettyDate := FormatDateTime('dddddd', LNow, TFormatSettings.Create('en-GB'));
var HtmlDate := Format('<time datetime="%s">%s</time>', [DateToISO8601(LNow, True), PrettyDate]);
var Html := Template
.Replace('%CONTENT%', string.Join(#13#10, MainContents.ToArray))
.Replace('%HEADER%', LHeader)
.Replace('%FOOTER%', LFooter)
.Replace('%TITLE%', 'Enter a topic')
.Replace('%AUTHOR%', 'Algosim Documentation Compiler')
.Replace('%MODIFIED%', HtmlDate);
TFile.WriteAllText(HtmlFileName, Html, TEncoding.UTF8);
finally
UrlDict.Free;
end;
finally
MainContents.Free;
end;
end;
class procedure TASDoc.CreateHypertextIndex;
begin
var HypertextRoot := TPath.Combine(Path, 'html');
var HtmlFileName := TPath.Combine(HypertextRoot, 'docindex.html');
var LHeader := '';
if TFile.Exists(TPath.Combine(Path, 'header.inc')) then
LHeader := TFile.ReadAllText(TPath.Combine(Path, 'header.inc'), TEncoding.UTF8);
var LFooter := '';
if TFile.Exists(TPath.Combine(Path, 'footer.inc')) then
LFooter := TFile.ReadAllText(TPath.Combine(Path, 'footer.inc'), TEncoding.UTF8);
var MainContents := TList<string>.Create;
try
MainContents.Add('<ul class="helpindex">');
var PrevLetter := #0;
for var Topic in GetTopics do
begin
if Topic.IsEmpty then
Continue;
var FirstLetter := Topic[1].ToUpper;
if not CharInSet(FirstLetter, ['A'..'Z']) then
FirstLetter := '#';
if FirstLetter <> PrevLetter then
MainContents.Add(Format(' <li class="indexheader">%s</li>', [string(FirstLetter)]));
var LinkURL := UrlEncode(TPath.GetFileNameWithoutExtension(TASDoc.GetDocFileNameForWeb(Topic, True))) + '.html';
MainContents.Add(Format(' <li><a href="%s">%s</a></li>', [LinkURL, HtmlEscape(Topic)]));
PrevLetter := FirstLetter;
end;
MainContents.Add('</ul>');
var LNow := TTimeZone.Local.ToUniversalTime(Now);
var PrettyDate := FormatDateTime('dddddd', LNow, TFormatSettings.Create('en-GB'));
var HtmlDate := Format('<time datetime="%s">%s</time>', [DateToISO8601(LNow, True), PrettyDate]);
var Html := Template
.Replace('%CONTENT%', string.Join(#13#10, MainContents.ToArray))
.Replace('%HEADER%', LHeader)
.Replace('%FOOTER%', LFooter)
.Replace('%TITLE%', 'Help index')
.Replace('%AUTHOR%', 'Algosim Documentation Compiler')
.Replace('%MODIFIED%', HtmlDate);
TFile.WriteAllText(HtmlFileName, Html, TEncoding.UTF8);
finally
MainContents.Free;
end;
end;
class procedure TASDoc.CreateHypertextVersion(const ATopic: string);
var
DocFileName,
DocFileNameForWeb: string;
HypertextRoot: string;
Zip: TZipFile;
TextBytes: TBytes;
TextLines: TArray<string>;
MetadataBytes: TBytes;
MetadataLines: TArray<string>;
HtmlFileName: string;
Html, MainContent: string;
MainContentLines: TStringList;
CurrentParagraph: string;
InInput, InOutput, InParagraph, InPre,
InBulletList, InBulletListInner: Boolean;
InBulletItem, InBulletItemInner: Boolean;
ExecInput, Generate, Button: Boolean;
InTable: Boolean;
TableLastRow: Integer;
Flags: TDictionary<string, string>;
function ConsumeID(out ID: string): Boolean;
begin
Result := Flags.TryGetValue('id', ID);
if Result then
Flags.Remove('id');
end;
function MetadataPart(const APartID: string): string;
begin
for var i := 0 to High(MetadataLines) do
if MetadataLines[i].StartsWith(APartID + ':', True) then
Exit(Copy(MetadataLines[i], APartID.Length + 2).Trim);
Result := '';
end;
function HtmlFormatDateTime(const S: string): string;
procedure Inv;
begin
raise EDocException.CreateFmt('HtmlFormatDateTime: invalid string "%s".', [S]);
end;
begin
var Parts := S.Split([#32]);
Result := '';
if Length(Parts) <> 2 then Inv;
var DateParts := Parts[0].Split(['-']);
if Length(DateParts) <> 3 then Inv;
var TimeParts := Parts[1].Split([':']);
if Length(TimeParts) <> 3 then Inv;
var Y, M, D: Integer;
if not TryStrToInt(DateParts[0], Y) then Inv;
if not TryStrToInt(DateParts[1], M) then Inv;
if not TryStrToInt(DateParts[2], D) then Inv;
var PrettyDate := FormatDateTime('dddddd', EncodeDate(Y, M, D), TFormatSettings.Create('en-GB'));
Result := Format('<time datetime="%s">%s</time>', [S, PrettyDate]);
end;
function TextLevelFmt(const S: string): string;
var
Output: TString;
InLink, InLinkURL, InCode: Boolean;
Anchor: Integer;
LinkCaption, LinkURL: string;
IgnoreNext: Boolean;
procedure LinkEnd;
begin
if not LinkCaption.IsEmpty then
begin
if LinkURL.IsEmpty then
LinkURL := LinkCaption;
if not IsURL(LinkURL) then
LinkURL := UrlEncode(TPath.GetFileNameWithoutExtension(TASDoc.GetDocFileNameForWeb(LinkURL, True))) + '.html';
Output.Append(Format('<a href="%s">%s</a>', [LinkURL, HtmlEscape(LinkCaption)]));
end;
InLink := False;
InLinkURL := False;
LinkCaption := '';
LinkURL := '';
end;
begin
Output := TString.Create(Round(1.5 * S.Length));
InLink := False;
InLinkURL := False;
InCode := False;
Anchor := 0;
LinkCaption := '';
LinkURL := '';
IgnoreNext := False;
for var i := 1 to S.Length do
begin
if IgnoreNext then
IgnoreNext := False
else if InLinkURL then
begin
if s[i] = '›' then
begin
LinkURL := Copy(S, Anchor, i - Anchor);
LinkEnd;
end;
end
else if InLink then
begin
if (S[i] = '›') or ((S[i] = '|') and (i > Anchor)) then
begin
LinkCaption := Copy(S, Anchor, i - Anchor);
if S[i] = '|' then
begin
Anchor := Succ(i);
InLinkURL := True;
end
else
LinkEnd;
end
end
else if s[i] = '‹' then
begin
InLink := True;
Anchor := Succ(i);
LinkCaption := '';
LinkURL := '';
end
else if S[i] = '`' then
begin
if (i < S.Length) and (S[Succ(i)] = '`') then
begin
Output.Append(S[i]);
IgnoreNext := True;
end
else
begin
InCode := not InCode;
if InCode then
Output.Append('<code>')
else
Output.Append('</code>')
end;
end
else
Output.Append(HtmlEscape(S[i]));
end;
Result := string(Output);
if InCode or InLink or InLinkURL then
raise EDocException.CreateFmt('Syntax error in "%s".', [ATopic]);
if (Result.Length = 1) and Result[1].IsSymbol then
Result := Result[1] + #$FE0E;
end;
procedure ParagraphEnd;
begin
if CurrentParagraph.IsEmpty then Exit;
MainContentLines.Add(Format('<p>%s</p>', [TextLevelFmt(CurrentParagraph)]));
InParagraph := False;
CurrentParagraph := '';
end;
procedure PreEnd;
begin
MainContentLines.Add(Format('<pre>%s</pre>', [TextLevelFmt(CurrentParagraph)]));
InPre := False;
CurrentParagraph := '';
end;
procedure BulletParagraphEnd;
begin
if CurrentParagraph.IsEmpty then Exit;
if InBulletListInner then
MainContentLines.Add(Format(' <p>%s</p>', [TextLevelFmt(CurrentParagraph)]))
else if InBulletList then
MainContentLines.Add(Format(' <p>%s</p>', [TextLevelFmt(CurrentParagraph)]));
CurrentParagraph := '';
end;
procedure BulletItemEnd;
begin
BulletParagraphEnd;
if InBulletItemInner then
begin
MainContentLines.Add(' </li>');
InBulletItemInner := False;
end
else if InBulletItem then
begin
MainContentLines.Add(' </li>');
InBulletItem := False;
end;
end;
procedure BulletListEnd;
begin
BulletItemEnd;
if InBulletListInner then
begin
MainContentLines.Add(' </ul>');
InBulletListInner := False;
end
else if InBulletList then
begin
MainContentLines.Add('</ul>');
InBulletList := False;
end;
end;
var
LKernel: TASKernel;
procedure RunScript(const AScript: string; ATextLevelParsing: Boolean = False);
var
Obj: TAlgosimObject;
S, L: string;
begin
if LKernel = nil then
begin
LKernel := TASKernel.Create;
LKernel.PropStore.AddSubstore(TFrontEndProperties.Create(AlgosimMainForm));
end;
Obj := LKernel.Evaluate(AScript);
if Obj is TAlgosimPixmap then
begin
var LImageFileName := Hash.THashBobJenkins.GetHashString(ATopic + AScript) + '.png';
var LImageFilePath := TPath.Combine(
TPath.Combine(HypertextRoot, 'images'),
LImageFileName
);
if FileExists(LImageFilePath) then
raise EDocException.Create('Image file name collision.');
TAlgosimPixmap(Obj).Value.SaveToFile(LImageFilePath);
MainContentLines.Add(Format('<p><img src="%s" alt="%s" /></p>', [
'images/' + LImageFileName,
HtmlEscape(Obj.ToString).Replace('"', '"')
]));
end
else if Obj is TAlgosimSound then
begin
var LAudioFileName := Hash.THashBobJenkins.GetHashString(ATopic + AScript) + '.wav';
var LAudioFilePath := TPath.Combine(
TPath.Combine(HypertextRoot, 'audio'),
LAudioFileName
);
if FileExists(LAudioFilePath) then
raise EDocException.Create('Audio file name collision.');
if TAlgosimSound(Obj).Value.BitsPerSample > 16 then
TAlgosimSound(Obj).Value.ConvertTo(16).SaveToFile(LAudioFilePath)
else
TAlgosimSound(Obj).Value.SaveToFile(LAudioFilePath);
MainContentLines.Add(Format('<p><audio controls src="%s">%s</audio></p>', [
'audio/' + LAudioFileName,
HtmlEscape(Obj.ToString).Replace('"', '"')
]));
end
else if (Obj is TAlgosimTable) and ATextLevelParsing then
begin
var tbl := TAlgosimTable(Obj).Value;
var ID: string;
if (tbl.Width = 2) and Flags.ContainsKey('dl') then
begin
if ConsumeID(ID) then
MainContentLines.Add(Format('<dl id="%s">', [ID]))
else
MainContentLines.Add('<dl>');
for var y := 0 to tbl.Height - 1 do
begin
MainContentLines.Add(Format(' <dt>%s</dt>', [TextLevelFmt(tbl[Point(0, y)])]));
MainContentLines.Add(Format(' <dd>%s</dd>', [TextLevelFmt(tbl[Point(1, y)])]));
end;
MainContentLines.Add('</dl>');
end
else
begin
if ConsumeID(ID) then
MainContentLines.Add(Format('<table class="grid" id="%s">', [ID]))
else
MainContentLines.Add('<table class="grid">');
for var y := 0 to tbl.Height - 1 do
begin
MainContentLines.Add(' <tr>');
for var x := 0 to tbl.Width - 1 do
begin
MainContentLines.Add(Format(' <td>%s</td>', [TextLevelFmt(tbl[Point(x, y)])]));
end;
MainContentLines.Add(' </tr>');
end;
MainContentLines.Add('</table>');
end
end
else
begin
S := Obj.ExplainedOutput(DefaultFormatOptions);
if ATextLevelParsing then
MainContentLines.Add(Format('<p>%s</p>', [TextLevelFmt(S).Replace(#13#10#13#10, '</p><p>')]))
else
begin
MainContentLines.Add('<pre class="output">');
for L in S.Split([sLineBreak]) do
MainContentLines.Add(TextLevelFmt(L));
MainContentLines.Add('</pre>');
end;
end;
end;
procedure IncludePrerenderedFile(const AScript: string);
begin
var LFileName := Hash.THashBobJenkins.GetHashString(ATopic + #13#10 + AScript) + '.wav';
var LFilePath := TPath.Combine(
TPath.Combine(HypertextRoot, 'audio'),
LFileName
);
var LSourcePath := TPath.Combine(
TPath.Combine(TASDoc.FPath, 'prerendered'),
LFileName
);
if FileExists(LFilePath) then
raise EDocException.Create('Audio file name collision.');
if FileExists(LSourcePath) then
begin
TFile.Copy(LSourcePath, LFilePath);
MainContentLines.Add(Format('<p><audio controls src="%s">%s</audio></p>', [
'audio/' + LFileName,
HtmlEscape('Pre-recorded output of audio.').Replace('"', '"')
]));
end
else if True then
begin
var S := AScript + #13#10#13#10 + LFileName;
Clipboard.AsText := S;
MessageBox(0, PChar(S), 'Missing Prerendered File', MB_ICONINFORMATION);
end
end;
procedure ParseTable(ATableStart: Integer);
const
TableChars: array[0..10] of Char = '┌─┬┐│├┼┤└┴┘';
function BorderOnlyRow(AIndex: Integer): Boolean;
begin
var S := TextLines[AIndex].Trim;
for var i := 1 to S.Length do
if not S[i].IsInArray(TableChars) then
Exit(False);
Result := True;
end;
function FirstColEmpty(AIndex: Integer): Boolean;
begin
Result := True;
var LWallCount := 0;
for var i := 1 to TextLines[AIndex].Length do
begin
if TextLines[AIndex][i] = '│' then
begin
Inc(LWallCount);
if LWallCount = 2 then
Exit;
end
else if not TextLines[AIndex][i].IsWhiteSpace then
Exit(False);
end;
end;
var
LHeaderRow: Boolean;
LImplicitRows: Boolean;
LHeaderCol: Boolean;
LInTableRow: Boolean;
LRowHasContent: Boolean;
LRowIndex: Integer;
Row: TList<string>;
WallCount, NewWallCount: Integer;
FloorCount: Integer;
HasDivider: Boolean;
RowCaption, ColCaption: string;
LInCol: Boolean;
LColIndex: Integer;
LColStartIdx: Integer;
procedure WriteRow;
begin
MainContentLines.Add(' <tr>');
for var i := 1 to Row.Count do
begin
var LIsHeader :=
LHeaderRow and (LRowIndex = 1)
or
LHeaderCol and (i = 1);
if LIsHeader then
if HasDivider and (LRowIndex = 1) and (i = 1) then
MainContentLines.Add(' <th class="superheader">')
else
MainContentLines.Add(' <th>')
else
MainContentLines.Add(' <td>');
if HasDivider and (LRowIndex = 1) and (i = 1) then
MainContentLines.Add(Format(
'<div><span class="rows" title="Rows">%s</span><span class="cols" title="Columns">%s</span></div>',
[RowCaption.Trim, ColCaption.Trim]
))
else
MainContentLines.Add(' ' + TextLevelFmt(Row[i - 1]).Replace(#13#10, '<br/>'));
if LIsHeader then
MainContentLines.Add(' </th>')
else
MainContentLines.Add(' </td>')
end;
MainContentLines.Add(' </tr>');
Row.Clear;
end;
function LeadingWhitespaceCount(const S: string): Integer;
begin
Result := 0;
while (Result + 1 <= S.Length) and S[Result + 1].IsWhiteSpace do
Inc(Result);
end;
begin
InTable := True;
TableLastRow := High(TextLines);
for var i := ATableStart to High(TextLines) do
if TextLines[i].IndexOfAny(TableChars) = -1 then
begin
TableLastRow := Pred(i);
Break;
end;
begin
var c := 80;
for var i := ATableStart to TableLastRow do
c := Min(c, LeadingWhitespaceCount(TextLines[i]));
for var i := ATableStart to TableLastRow do
Delete(TextLines[i], 1, c);
end;
WallCount := 0;
FloorCount := 0;
HasDivider := False;
for var i := ATableStart to TableLastRow do
begin
NewWallCount := TextLines[i].CountChar('│');
if NewWallCount > WallCount then
WallCount := NewWallCount;
if BorderOnlyRow(i) then
Inc(FloorCount);
if (FloorCount <= 1) and not HasDivider then
begin
var DPos := Pos('╲', TextLines[i]);
if DPos <> 0 then
begin
var WallsBefore := 0;
for var j := 1 to DPos - 1 do
begin
if TextLines[i][j] = '│' then
Inc(WallsBefore);
if WallsBefore > 1 then
Break;
end;
if WallsBefore <= 1 then
HasDivider := True;
end;
end;
end;
if HasDivider then
begin
var LFloorCount := 0;
for var i := ATableStart to TableLastRow do
begin
if TextLines[i].Contains('─') then
Inc(LFloorCount);
if LFloorCount >= 2 then
Break;
var DPos := Pos('╲', TextLines[i]);
var WPos1 := Pos('│', TextLines[i]);
var WPos2 := Pos('│', TextLines[i], Succ(WPos1));
if DPos > 0 then
if InRange(DPos, WPos1, WPos2) then
begin
RowCaption := RowCaption + Copy(TextLines[i], Succ(WPos1), DPos - WPos1 - 1);
ColCaption := ColCaption + Copy(TextLines[i], Succ(DPos), WPos2 - DPos - 1);
end
else if DPos < WPos1 then
begin
RowCaption := RowCaption + Copy(TextLines[i], 1, DPos - 1);
ColCaption := ColCaption + Copy(TextLines[i], Succ(DPos), WPos1 - DPos - 1);
end;
end;
end;
LHeaderRow := FloorCount = 3;
LImplicitRows := (FloorCount = 3) and (WallCount >= 3);
LHeaderCol := (WallCount >= 3) and HasDivider;
LInTableRow := False;
LRowHasContent := False;
LRowIndex := 0;
var BorderOnlyRowCount := 0;
var LAfterHeader := False;
Row := TList<string>.Create;
try
MainContentLines.Add('<table>');
for var i := ATableStart to TableLastRow do
begin
var LBorderOnlyRow := BorderOnlyRow(i);
if LBorderOnlyRow then
Inc(BorderOnlyRowCount);
if BorderOnlyRowCount >= 2 then
LAfterHeader := True;
if
LInTableRow and LRowHasContent and LBorderOnlyRow
or
LInTableRow and LRowHasContent and LAfterHeader and LImplicitRows and not FirstColEmpty(i)
then
begin
WriteRow;
LInTableRow := False;
LRowHasContent := False;
end;
if LBorderOnlyRow then
Continue;
if not LInTableRow then
begin
Row.Clear;
LInTableRow := True;
LRowHasContent := False;
Inc(LRowIndex);
end;
LInCol := False;
LColIndex := 0;
LColStartIdx := 0;
for var j := 1 to TextLines[i].Length do
begin
if (TextLines[i][j] = '│') and LInCol then
begin
var S: string;
S := Copy(TextLines[i], LColStartIdx, j - LColStartIdx).Trim;
if LColIndex - 1 <= Row.Count - 1 then
if Row[LColIndex - 1].Trim.IsEmpty or S.IsEmpty then
Row[LColIndex - 1] := Row[LColIndex - 1] + S
else
Row[LColIndex - 1] := Row[LColIndex - 1] + #13#10 + S
else
Row.Add(S);
LInCol := False;
end;
if not TextLines[i][j].IsInArray(TableChars) then
begin
if not LInCol then
begin
LInCol := True;
Inc(LColIndex);
LColStartIdx := j;
end;
if not TextLines[i][j].IsWhiteSpace then
LRowHasContent := True;
end;
end;
if LInCol then
begin
var S: string;
S := Copy(TextLines[i], LColStartIdx).Trim;
if LColIndex - 1 <= Row.Count - 1 then
Row[LColIndex - 1] := Row[LColIndex - 1] + S
else
Row.Add(S);
end;
end;
if LInTableRow then
WriteRow;
MainContentLines.Add('</table>');
finally
Row.Free;
end;
end;
begin
var LImgCounter := 0;
DocFileName := GetDocFileName(ATopic, True);
DocFileNameForWeb := GetDocFileNameForWeb(ATopic, True);
HypertextRoot := TPath.Combine(Path, 'html');
HtmlFileName := TPath.Combine(HypertextRoot, DocFileNameForWeb);
ForceDirectories(HypertextRoot);
ForceDirectories(TPath.Combine(HypertextRoot, 'images'));
ForceDirectories(TPath.Combine(HypertextRoot, 'audio'));
Flags := TDictionary<string, string>.Create;
try
LKernel := nil;
try
Zip := TZipFile.Create;
try
Zip.Open(DocFileName, zmRead);
try
Zip.Read('text.asml', TextBytes);
TextLines := TEncoding.UTF8.GetString(TextBytes).Split([sLineBreak]);
Zip.Read('metadata.txt', MetadataBytes);
MetadataLines := TEncoding.UTF8.GetString(MetadataBytes).Split([sLineBreak]);
MainContentLines := TStringList.Create;
try
InInput := False;
InOutput := False;
InParagraph := False;
InPre := False;
InBulletList := False;
InBulletListInner := False;
InBulletItem := False;
InBulletItemInner := False;
ExecInput := False;
for var i := 0 to High(TextLines) do
begin
if TextLines[i].StartsWith('~html') then
begin
MainContentLines.Add(Copy(TextLines[i], 6).Trim);
Continue;
end;
if TextLines[i].StartsWith('~set') then
begin
var EqPos := Pos('=', TextLines[i]);
if EqPos = 0 then
Flags.AddOrSetValue(Copy(TextLines[i], 5).Trim, '')
else
Flags.AddOrSetValue(Copy(TextLines[i], 6, EqPos - 6).Trim, Copy(TextLines[i], EqPos + 1).Trim);
Continue;
end;
if TextLines[i].StartsWith('~clear') then
begin
Flags.Remove(Copy(TextLines[i], 5).Trim);
Continue;
end;
if TextLines[i].StartsWith('~img') then
Continue;
if InTable and (i <= TableLastRow) then
Continue;
if TextLines[i].StartsWith(' ‣ is an lvalue') then
TextLines[i] := 'The returned value is an lvalue' + Copy(TextLines[i], 17) + '.';
if InParagraph then
begin
if TextLines[i].Trim.IsEmpty or TextLines[i][1].IsInArray(['<', '>', '!', '§', #$FFFC, '━']) or TextLines[i].StartsWith(#32#32) then
ParagraphEnd
else
CurrentParagraph := CurrentParagraph + IfThen(not CurrentParagraph.EndsWith('-'), #32) + TextLines[i].Trim
end;
if InPre then
begin
if not TextLines[i].StartsWith(#32#32) then
PreEnd
else
CurrentParagraph := CurrentParagraph + #13#10 + Copy(TextLines[i], 3).TrimRight;
end;
if InBulletListInner then
begin
if TextLines[i].StartsWith(' -') then
BulletItemEnd
else if TextLines[i].StartsWith(' ') and not TextLines[i].Trim.IsEmpty then
CurrentParagraph := CurrentParagraph + IfThen(not CurrentParagraph.EndsWith('-'), #32) + TextLines[i].Trim
else if TextLines[i].Trim.IsEmpty and (i < High(TextLines)) and TextLines[Succ(i)].StartsWith(' ') then
BulletParagraphEnd
else if TextLines[i].StartsWith('•') or TextLines[i].Trim.IsEmpty and (i < High(TextLines)) and TextLines[Succ(i)].StartsWith('•') then
begin
BulletListEnd;
BulletItemEnd;
end
else
begin
BulletListEnd;
BulletListEnd;
end;
end
else if InBulletList then
begin
if TextLines[i].StartsWith('•') then
BulletItemEnd
else if TextLines[i].StartsWith(' -') then
BulletParagraphEnd
else if TextLines[i].StartsWith(' ') and not TextLines[i].Trim.IsEmpty then
CurrentParagraph := CurrentParagraph + IfThen(not CurrentParagraph.EndsWith('-'), #32) + TextLines[i].Trim
else if TextLines[i].Trim.IsEmpty and (i < High(TextLines)) and TextLines[Succ(i)].StartsWith(' ') then
BulletParagraphEnd
else if TextLines[i].Trim.IsEmpty and (i < High(TextLines)) and TextLines[Succ(i)].StartsWith('•') then
else
BulletListEnd
end;
if InInput then
begin
if TextLines[i].StartsWith('>>') then
begin
MainContentLines.Add('</pre>');
InInput := False;
end
else
MainContentLines.Add(TextLines[i].TrimRight);
end
else if InOutput then
begin
if TextLines[i].StartsWith('<<') then
begin
MainContentLines.Add('</pre>');
InOutput := False;
end
else
MainContentLines.Add(TextLines[i].TrimRight);
end
else if ExecInput then
begin
if TextLines[i].StartsWith('>>') then
begin
if not Generate then
begin
MainContentLines.Add('<pre class="input">');
for var ln in CurrentParagraph.Split([#13#10]) do
MainContentLines.Add(HtmlEscape(ln));
MainContentLines.Add('</pre>');
end;
if Button then
IncludePrerenderedFile(CurrentParagraph)
else
RunScript(CurrentParagraph, Generate);
ExecInput := False;
Generate := False;
Button := False;
end
else
if CurrentParagraph.IsEmpty then
CurrentParagraph := TextLines[i]
else
CurrentParagraph := CurrentParagraph + #13#10 + TextLines[i];
end
else
begin
var hn := 0;
while (hn < TextLines[i].Length) and (TextLines[i][hn + 1] = '§') and (hn < 6) do
Inc(hn);
if hn > 0 then
begin
MainContentLines.Add('');
MainContentLines.Add(Format('<h%d>%s</h%d>', [hn, TextLevelFmt(Copy(TextLines[i], hn + 1).Trim), hn]))
end
else if TextLines[i].StartsWith('>>@') then
begin
ExecInput := True;
Generate := True;
Button := False;
CurrentParagraph := '';
end
else if TextLines[i].StartsWith('>>>') then
begin
ExecInput := True;
Generate := False;
Button := False;
CurrentParagraph := '';
end
else if TextLines[i].StartsWith('>>!') then
begin
ExecInput := True;
Generate := False;
Button := True;
CurrentParagraph := '';
end
else if TextLines[i].StartsWith('>>') then
begin
MainContentLines.Add('<pre class="input">');
InInput := True;
end
else if TextLines[i].StartsWith('<<') then
begin
MainContentLines.Add('<pre class="output">');
InOutput := True;
end
else if TextLines[i].StartsWith('>') then
MainContentLines.Add(Format('<pre class="input">%s</pre>', [HtmlEscape(Copy(TextLines[i], 2).Trim)]))
else if TextLines[i].StartsWith('<') then
MainContentLines.Add(Format('<pre class="output">%s</pre>', [HtmlEscape(Copy(TextLines[i], 2).Trim)]))
else if TextLines[i].StartsWith('!') then
MainContentLines.Add(Format('<pre class="error">%s</pre>', [HtmlEscape(Copy(TextLines[i], 2).Trim)]))
else if TextLines[i].StartsWith('•') then
begin
if not InBulletList then
begin
MainContentLines.Add('<ul>');
InBulletList := True;
end;
MainContentLines.Add(' <li>');
InBulletItem := True;
CurrentParagraph := Copy(TextLines[i], 2).Trim;
end
else if InBulletList and TextLines[i].StartsWith(' -') then
begin
if not InBulletListInner then
begin
MainContentLines.Add(' <ul>');
InBulletListInner := True;
end;
MainContentLines.Add(' <li>');
InBulletItemInner := True;
CurrentParagraph := Copy(TextLines[i], 4).Trim;
end
else if TextLines[i].StartsWith(#$FFFC#$FFFF) then
begin
if (i < High(TextLines)) and TextLines[Succ(i)].StartsWith('~img') then
begin
var LImgSpec := TextLines[Succ(i)].Split([#32]);
if Length(LImgSpec) < 2 then
raise EDocException.Create('Incomplete ~img directive.');
var LImgFileName := LImgSpec[1];
var LImgSourcePath := TPath.Combine(TPath.Combine(TASDoc.FPath, 'prerendered'), LImgFileName + '.svg');
if not FileExists(LImgSourcePath) then
raise EDocException.CreateFmt('Missing SVG image file: %s', [LImgFileName]);
var LImgDestPath := TPath.Combine(TPath.Combine(HypertextRoot, 'images'), LImgFileName + '.svg');
TFile.Copy(LImgSourcePath, LImgDestPath, True);
Inc(LImgCounter);
MainContentLines.Add(Format('<p><img src="images/%s" alt="Image %d" /></p>', [LImgFileName + '.svg', LImgCounter]));
end
else
begin
var ObjIdx := StrToInt(Copy(TextLines[i], 3).Trim);
var LImageBytes: TBytes;
var LImageFileName := Hash.THashBobJenkins.GetHashString(ATopic + ObjIdx.ToString) + '.png';
var LImageFilePath := TPath.Combine(
TPath.Combine(HypertextRoot, 'images'),
LImageFileName
);
if FileExists(LImageFilePath) then
raise EDocException.Create('Image file name collision.');
Zip.Read('image' + ObjIdx.ToString + '.png', LImageBytes);
TFile.WriteAllBytes(LImageFilePath, LImageBytes);
Inc(LImgCounter);
MainContentLines.Add(Format('<p><img src="images/%s" alt="Image %d" /></p>', [LImageFileName, LImgCounter]));
end;
end
else if TextLines[i] = StringOfChar('━', 80) then
MainContentLines.Add('<hr/>')
else if TextLines[i].Contains('─') then
ParseTable(i)
else if not InPre and not InParagraph and not InBulletList and TextLines[i].StartsWith(#32#32) and not TextLines[i].Trim.IsEmpty then
begin
InPre := True;
CurrentParagraph := Copy(TextLines[i], 3).TrimRight;
end
else if not InPre and not InParagraph and not InBulletList and not TextLines[i].Trim.IsEmpty then
begin
InParagraph := True;
CurrentParagraph := TextLines[i].Trim;
end;
end;
end;
if InParagraph then
ParagraphEnd;
if InPre then
PreEnd;
if InBulletListInner then
MainContentLines.Add(' </ul>');
if InBulletList then
BulletListEnd;
if not Flags.ContainsKey('noarticle') then
begin
MainContentLines.Insert(0, '<article>');
MainContentLines.Add('</article>');
end;
MainContent := MainContentLines.Text;
finally
MainContentLines.Free;
end;
var LHeader := '';
if TFile.Exists(TPath.Combine(Path, 'header.inc')) then
LHeader := TFile.ReadAllText(TPath.Combine(Path, 'header.inc'), TEncoding.UTF8);
var LFooter := '';
if TFile.Exists(TPath.Combine(Path, 'footer.inc')) then
LFooter := TFile.ReadAllText(TPath.Combine(Path, 'footer.inc'), TEncoding.UTF8);
Html := Template
.Replace('%CONTENT%', MainContent)
.Replace('%HEADER%', LHeader)
.Replace('%FOOTER%', LFooter)
.Replace('%TITLE%', ATopic)
.Replace('%AUTHOR%', MetadataPart('Author'))
.Replace('%MODIFIED%', HtmlFormatDateTime(MetadataPart('Modified')));
TFile.WriteAllText(HtmlFileName, Html, TEncoding.UTF8);
finally
Zip.Close;
end;
finally
Zip.Free;
end
finally
LKernel.Free;
end;
finally
Flags.Free;
end;
end;
procedure DeleteDirectory(const ADir: string);
var
FileOp: TSHFileOpStruct;
begin
FillChar(FileOp, SizeOf(FileOp), 0);
FileOp.wFunc := FO_DELETE;
FileOp.pFrom := PChar(ADir + #0);
FileOp.fFlags := FOF_NOCONFIRMATION;
SHFileOperation(FileOp);
end;
class procedure TASDoc.CreateHypertextVersion;
begin
var htmlp := TPath.Combine(Path, 'html');
AllocConsole;
try
DeleteDirectory(htmlp);
if DirectoryExists(htmlp) then
raise EDocException.Create('Target not clean.');
try
for var Topic in GetTopics do
begin
Writeln(Topic);
CreateHypertextVersion(Topic);
end;
TFile.Copy(TPath.Combine(Path, 'asdoc.css'), TPath.Combine(htmlp, 'asdoc.css'), True);
CreateHypertextIndex;
CreateHypertextGotoPage;
except
DeleteDirectory(htmlp);
raise;
end;
finally
FreeConsole;
end;
end;
class function TASDoc.GetDocFileName(const ATopic: string;
ARaiseOnNotFound: Boolean): string;
var
i: Integer;
begin
Result := SanitizeFileName(ATopic);
if Result.IsEmpty and (ATopic.Length = 1) then
Result := SanitizeFileName(UCD.GetChrName(ATopic[1]));
if Result.IsEmpty then
begin
Result := '';
for i := 1 to ATopic.Length do
Result := Result + Ord(ATopic[i]).ToHexString;
end;
if Result.IsEmpty then
raise EDocException.CreateFmt('Couldn''t create a file name for "%s".', [ATopic]);
Result := TPath.Combine(Path, Result + '.asdoc');
if ARaiseOnNotFound and not FileExists(Result) then
raise EDocException.CreateFmt('Topic "%s" not found.', [ATopic]);
end;
class function TASDoc.GetDocFileNameForWeb(const ATopic: string;
ARaiseOnNotFound: Boolean): string;
begin
Result := TPath.GetFileNameWithoutExtension(GetDocFileName(ATopic, ARaiseOnNotFound)) + '.html';
end;
class function TASDoc.GetMetadata: string;
var
Size: Cardinal;
UserName: string;
begin
UserName := '';
if not GetUserName(nil, Size) and (GetLastError = ERROR_INSUFFICIENT_BUFFER) then
begin
SetLength(UserName, Size);
if GetUserName(PChar(UserName), Size) and (Size > 0) then
SetLength(UserName, Size - 1);
end
else
begin
Size := 256 + 1;
SetLength(UserName, Size);
if GetUserName(PChar(UserName), Size) and (Size > 0) then
SetLength(UserName, Size - 1);
end;
Result :=
'Author: ' + UserName + sLineBreak +
'Modified: ' + FormatDateTime('YYYY"-"MM"-"DD" "hh":"nn":"ss"."zzz"Z"',
TTimeZone.Local.ToUniversalTime(Now),
TFormatSettings.Invariant) + sLineBreak +
'Version: ' + Version;
end;
class function TASDoc.GetTopicName(const AFileName: TFileName): string;
var
Zip: TZipFile;
NameBytes: TBytes;
begin
Zip := TZipFile.Create;
try
Zip.Open(AFileName, zmRead);
try
if Zip.IndexOf('name.txt') = -1 then
Exit(TPath.GetFileNameWithoutExtension(AFileName));
Zip.Read('name.txt', NameBytes);
Result := TEncoding.UTF8.GetString(NameBytes);
finally
Zip.Close;
end;
finally
Zip.Free;
end;
end;
class function TASDoc.GetTopics: TArray<string>;
var
TopicList: TList<string>;
s: string;
begin
RequireDoc;
TopicList := TList<string>.Create;
try
for s in TFile.ReadAllLines(TPath.Combine(FPath, 'index.rdx'), TEncoding.UTF8) do
TopicList.Add(Copy(s, Succ(Pos(#9, s))));
TopicList.Sort(TComparer<string>.Construct(
function(const ALeft, ARight: string): Integer
function IsSymbol(const S: string): Boolean;
begin
Result := (S.Length = 1) and (S[1].IsSymbol or S[1].IsPunctuation);
end;
begin
Result := CompareValue(Ord(IsSymbol(ALeft)), Ord(IsSymbol(ARight)));
if Result = 0 then
Result := CompareText(ALeft, ARight);
end
));
Result := TopicList.ToArray;
finally
TopicList.Free;
end;
end;
type
TScriptButton = class(TButton)
public
Script: string;
procedure Click; override;
end;
procedure TScriptButton.Click;
var
LKernel: TASKernel;
LObj: TAlgosimObject;
begin
inherited;
if Script.Trim.IsEmpty then
Exit;
Screen.Cursor := crHourglass;
try
LKernel := TASKernel.Create;
try
LObj := LKernel.Evaluate(Script);
if Assigned(LObj) and not (LObj is TAlgosimNullObject) then
TGallery.CreateFrame(LObj, '', True);
finally
LKernel.Free;
end;
finally
Screen.Cursor := crDefault;
end;
end;
class procedure TASDoc.LoadFromFile(AEditor: TTextEditor;
const AFileName: TFileName; out ATopicInfo: TTopicInfo; ADocMode: TDocMode);
resourcestring
SRunScriptBtnCaption = 'Run';
var
Zip: TZipFile;
NameBytes: TBytes;
MetadataBytes: TBytes;
TextBytes: TBytes;
i: Integer;
CtlId: Integer;
ImgStream: TStream;
LocalHeader: TZipHeader;
Png: TPngImage;
Bm: TBitmap;
Links: THyperlinks;
Objs: TObjectDictionary<Integer, TAlgosimObject>;
ObjID: Integer;
Obj: TAlgosimObject;
SoundPlayer: TSoundPlayer;
Button: TScriptButton;
begin
if AEditor = nil then
Exit;
AEditor.TextFile.StrictReadOnly := False;
AEditor.EditMode := emText;
AEditor.NewFile;
Links := THyperlinks.Create;
try
Objs := TObjectDictionary<Integer, TAlgosimObject>.Create([doOwnsValues]);
try
Zip := TZipFile.Create;
try
Zip.Open(AFileName, zmRead);
try
if Zip.IndexOf('text.asml') = -1 then
raise EDocException.Create('Invalid documentation file.');
Zip.Read('text.asml', TextBytes);
case ADocMode of
dmEdit:
AEditor.PlainText := TEncoding.UTF8.GetString(TextBytes);
dmView:
AEditor.TextFile := Parse(TEncoding.UTF8.GetString(TextBytes).Split([sLineBreak]),
Links, Objs);
else
raise EDocException.CreateFmt('Invalid document mode: %d', [Ord(ADocMode)]);
end;
for i := 0 to AEditor.LineCount - 1 do
begin
if AEditor.Lines[i].StartsWith(LINE_CONTROL_PREFIX) then
begin
AEditor.LineClasses[i] := LINE_CONTROL_CLASS;
CtlId := AEditor.GetControlIDFromLine(i);
if Zip.IndexOf(Format('image%d.png', [CtlId])) = -1 then
begin
AEditor.LineClasses[i] := '';
AEditor.Lines[i] := '[Missing image.]';
end
else
begin
Zip.Read(Format('image%d.png', [CtlId]), ImgStream, LocalHeader);
try
Png := TPngImage.Create;
try
Png.LoadFromStream(ImgStream);
AEditor.ReceiveGraphic(Png, i);
finally
Png.Free;
end;
finally
ImgStream.Free;
end;
end;
end
else if AEditor.Lines[i].StartsWith('<~<') then
begin
if TryStrToInt(Copy(AEditor.Lines[i], 4), ObjID) and Objs.TryGetValue(ObjID, Obj) then
begin
if Obj is TAlgosimPixmap then
begin
AEditor.Lines[i] := '';
Bm := TAlgosimPixmap(Obj).Value.CreateGDIBitmap;
try
AEditor.ReceiveGraphic(Bm, i, True);
finally
Bm.Free;
end;
end
else if Obj is TAlgosimSound then
begin
AEditor.Lines[i] := '';
SoundPlayer := TSoundPlayer.Create(AEditor);
SoundPlayer.Sound := TAlgosimSound(Obj).Value;
SoundPlayer.Height := SoundPlayer.PreferredHeight;
SoundPlayer.Width := 10 * SoundPlayer.PreferredHeight;
AEditor.ReceiveLineControl(SoundPlayer, i, True);
end;
end
else
AEditor.Lines[i] := '[Missing object.]';
end
else if AEditor.Lines[i].StartsWith('<!<') then
begin
Button := TScriptButton.Create(AEditor);
Button.Caption := SRunScriptBtnCaption;
Button.Script := Copy(AEditor.Lines[i], 4);
AEditor.ReceiveLineControl(Button, i, True);
end;
end;
if Zip.IndexOf('name.txt') = -1 then
ATopicInfo.Name := TPath.GetFileNameWithoutExtension(AFileName)
else
begin
Zip.Read('name.txt', NameBytes);
ATopicInfo.Name := TEncoding.UTF8.GetString(NameBytes);
end;
if Zip.IndexOf('metadata.txt') = -1 then
ATopicInfo.Metadata := ''
else
begin
Zip.Read('metadata.txt', MetadataBytes);
ATopicInfo.Metadata := TEncoding.UTF8.GetString(MetadataBytes);
end;
finally
Zip.Close;
end;
finally
Zip.Free;
end;
finally
Objs.Free;
end;
AEditor.MultiSize := True;
AEditor.RulerVisible := False;
if ADocMode = dmView then
begin
AEditor.ActivateLinks(Links);
Links := nil;
AEditor.EditMode := emReadOnly;
AEditor.TextFile.StrictReadOnly := True;
end;
AEditor.TextFile.GotoSOF;
AEditor.MakeUndoRoot;
finally
Links.Free;
end;
end;
class function TASDoc.Parse(const ASource: TArray<string>;
ALinks: THyperlinks; AObjects: TObjectDictionary<Integer, TAlgosimObject>): TTextFile;
var
TextFile: TTextFile absolute Result;
function TextFmt(const s: string): string;
var
i, c: Integer;
InLink: Boolean;
InLinkURL: Boolean;
Anchor: Integer;
LinkPos: TPoint;
LinkCaption: string;
LinkURL: string;
procedure LinkEnd;
begin
if Assigned(ALinks) then
begin
ALinks.Add(
THyperlink.Create(
LinkPos,
c - 1,
LinkCaption,
LinkURL
)
)
end;
InLink := False;
InLinkURL := False;
LinkPos := TPoint.Zero;
LinkCaption := '';
LinkURL := '';
end;
begin
SetLength(Result, s.Length);
c := 0;
i := 1;
InLink := False;
InLinkURL := False;
Anchor := 0;
while i <= s.Length do
begin
if InLinkURL then
begin
if s[i] = '›' then
begin
LinkURL := Copy(s, Anchor, i - Anchor);
LinkEnd;
end;
end
else if InLink then
begin
if (s[i] = '›') or ((s[i] = '|') and (i > Anchor)) then
begin
LinkCaption := Copy(s, Anchor, i - Anchor);
if s[i] = '|' then
begin
Anchor := Succ(i);
InLinkURL := True;
end
else
LinkEnd;
end
else
begin
Inc(c);
Result[c] := s[i];
end;
end
else if s[i] = '‹' then
begin
InLink := True;
Anchor := Succ(i);
LinkPos := Point(c, TextFile.LogicalLineCount);
LinkCaption := '';
LinkURL := '';
end
else if s[i] = '`' then
begin
if (i < s.Length) and (s[Succ(i)] = '`') then
begin
Inc(c);
Result[c] := s[i];
Inc(i);
end;
end
else
begin
Inc(c);
Result[c] := s[i];
end;
Inc(i);
end;
SetLength(Result, c);
end;
var
ObjID: Integer;
LKernel: TASKernel;
procedure AddButton(const AScript: string);
begin
Result.AddLine('<!<' + AScript.Replace(#13#10, #32));
end;
procedure RunScript(const AScript: string; ATextLevelParsing: Boolean = False);
var
Obj: TAlgosimObject;
S, L: string;
begin
if LKernel = nil then
begin
LKernel := TASKernel.Create;
LKernel.PropStore.AddSubstore(TFrontEndProperties.Create(AlgosimMainForm));
end;
try
Obj := LKernel.Evaluate(AScript);
if Assigned(AObjects) and ((Obj is TAlgosimPixmap) or (Obj is TAlgosimSound)) then
begin
AObjects.Add(ObjID, Obj.Clone);
Result.AddLine('<~<' + ObjId.ToString);
Inc(ObjID);
end
else
begin
S := Obj.ExplainedOutput(DefaultFormatOptions);
for L in S.Split([sLineBreak]) do
if ATextLevelParsing then
Result.AddLine(TextFmt(L))
else
Result.AddLine(L, 'Output');
end;
except
Result.AddLine('Couldn''t evaluate expression.', 'Error output');
end;
end;
var
i, j: Integer;
s: string;
InInput, InOutput, Execute, Button, Generate: Boolean;
Script: string;
begin
LKernel := nil;
ObjID := 0;
try
Result := TTextFile.Create;
try
Result.UseLineClasses := True;
Result.ControlAware := True;
Result.BeginAddLine;
try
InInput := False;
InOutput := False;
Execute := False;
Button := False;
Generate := False;
for i := 0 to High(ASource) do
begin
s := ASource[i];
if s.StartsWith('~html') or s.StartsWith('~set') or s.StartsWith('~clear') or s.StartsWith('~img') then
Continue;
if InInput then
begin
if S.Trim.StartsWith('>>') then
begin
InInput := False;
if Button then
AddButton(Script)
else if Execute or Generate then
RunScript(Script, Generate);
Execute := False;
Button := False;
Generate := False;
Continue;
end;
if not Generate then
Result.AddLine(S, 'Input');
if Button or Execute or Generate then
Script := Script + S + sLineBreak;
end
else if InOutput then
begin
if S.Trim = '<<' then
begin
InOutput := False;
Continue;
end;
Result.AddLine(S, 'Output');
end
else if s.Trim.IsEmpty then
Result.AddLine('')
else
case S[1] of
'§':
begin
j := 2;
while (j <= s.Length) and (s[j] = '§') do
Inc(j);
Dec(j);
Result.AddLine(TextFmt(Copy(s, Succ(j)).Trim),
Format('Heading %d', [j]));
end;
'>':
begin
if S.Trim = '>>@' then
begin
InInput := True;
Generate := True;
Script := '';
Continue;
end;
if S.Trim = '>>!' then
begin
InInput := True;
Button := True;
Script := '';
Continue;
end;
if S.Trim = '>>>' then
begin
InInput := True;
Execute := True;
Script := '';
Continue;
end;
if S.Trim = '>>' then
begin
InInput := True;
Continue;
end;
Result.AddLine(Copy(s, 2).Trim, 'Input');
end;
'<':
begin
if S.Trim = '<<' then
begin
InOutput := True;
Continue;
end;
Result.AddLine(Copy(s, 2).Trim, 'Output');
end;
'!':
begin
Result.AddLine(Copy(s, 2).Trim, 'Error output');
end;
else
Result.AddLine(TextFmt(s.TrimRight));
end;
end;
Result.AddLine('');
finally
Result.EndAddLine;
end;
except
Result.Free;
raise;
end;
finally
LKernel.Free;
end;
end;
class procedure TASDoc.PrepareViewer(AEditor: TTextEditor);
begin
if AEditor = nil then
Exit;
AEditor.LoadDefaultClasses;
AEditor.AddClass(MakeClass('Input', AEditor.Font.Size, [fsBold], AEditor.Font.Color));
AEditor.AddClass(MakeClass('Output', AEditor.Font.Size, [], AEditor.Font.Color));
AEditor.AddClass(MakeClass('Error output', AEditor.Font.Size, [], clRed));
end;
class procedure TASDoc.RequireDoc;
begin
if not TDirectory.Exists(Path) then
raise EDocException.Create('Documentation not installed.');
end;
class procedure TASDoc.SaveToFile(const ATopicName: string; AEditor: TTextEditor;
const AFileName: TFileName);
var
Zip: TZipFile;
i: Integer;
CtlId: Integer;
Ctl: TControl;
Png: TPngImage;
ImgStream: TMemoryStream;
begin
if AEditor = nil then
Exit;
AEditor.TidyControlIDs;
Zip := TZipFile.Create;
try
Zip.Open(AFileName, zmWrite);
try
Zip.Add(TEncoding.UTF8.GetBytes(AEditor.PlainText), 'text.asml');
Zip.Add(TEncoding.UTF8.GetBytes(ATopicName), 'name.txt');
Zip.Add(TEncoding.UTF8.GetBytes(GetMetadata), 'metadata.txt');
for i := 0 to AEditor.LineCount - 1 do
begin
CtlId := AEditor.GetControlIDFromLine(i);
if CtlId = -1 then
Continue;
Ctl := AEditor.GetControlFromID(CtlId);
if Ctl is TImage then
begin
Png := TPngImage.Create;
try
Png.Assign(TImage(Ctl).Picture.Graphic);
ImgStream := TMemoryStream.Create;
try
Png.SaveToStream(ImgStream);
ImgStream.Position := 0;
Zip.Add(ImgStream, Format('image%d.png', [CtlId]));
finally
ImgStream.Free;
end;
finally
Png.Free;
end;
end;
end;
finally
Zip.Close;
end;
finally
Zip.Free;
end;
end;
class function TASDoc.Search(const ASearchText: string;
ASearchOptions: TStringSearchOptions): TArray<string>;
var
fn: string;
Zip: TZipFile;
TextBytes: TArray<Byte>;
Text: string;
Lines: TArray<string>;
i, p: Integer;
Matches: TList<string>;
LSearchText: string;
begin
Result := nil;
if FFullTextIndex = nil then
begin
fn := TPath.Combine(FPath, 'fulltext.ftx');
if not TFile.Exists(fn) then
Exit;
Zip := TZipFile.Create;
try
Zip.Open(fn, zmRead);
try
if Zip.IndexOf('fulltext.txt') = -1 then
Exit;
Zip.Read('fulltext.txt', TextBytes);
Text := TEncoding.UTF8.GetString(TextBytes);
Lines := Text.Split([#13#10]);
SetLength(FFullTextIndex, Length(Lines));
for i := 0 to High(Lines) do
begin
p := Pos(#9, Lines[i]);
if p = 0 then
Continue;
FFullTextIndex[i].Key := Copy(Lines[i], 1, p - 1);
FFullTextIndex[i].Value := Copy(Lines[i], p + 1);
end;
finally
Zip.Close;
end;
finally
Zip.Free;
end;
end;
LSearchText := ProcessText(ASearchText);
Matches := TList<string>.Create;
try
for i := 0 to High(FFullTextIndex) do
if ASStrFcns.SubstringFirstIndex(LSearchText, FFullTextIndex[i].Value, ASearchOptions) <> 0 then
Matches.Add(FFullTextIndex[i].Key);
Result := Matches.ToArray;
finally
Matches.Free;
end;
end;
procedure TString.Append(C: Char);
begin
if FActualLength = FData.Length then
SetLength(FData, FData.Length + AllocStep);
Inc(FActualLength);
FData[FActualLength] := C;
end;
procedure TString.Append(const S: string);
begin
if S.IsEmpty then Exit;
if FActualLength + S.Length > FData.Length then
SetLength(FData, FActualLength + S.Length + AllocStep);
MoveChars(S[1], FData[FActualLength + 1], S.Length);
Inc(FActualLength, S.Length);
end;
constructor TString.Create(ACapacity: Integer);
begin
SetLength(FData, ACapacity);
FActualLength := 0;
end;
class operator TString.Explicit(const S: TString): string;
begin
SetLength(Result, S.FActualLength);
if Result.Length > 0 then
MoveChars(S.FData[1], Result[1], Result.Length);
end;
class operator TString.Implicit(const S: string): TString;
begin
Result.FData := S;
Result.FActualLength := S.Length;
end;
procedure TString.TrimExcess;
begin
SetLength(FData, FActualLength);
end;
end.