@@ -47,7 +47,7 @@
- DelphiDabbler CodeSnip is copyright © 2005-2020 by CodeSnip is copyright © 2005-2023 by Peter D Johnson.
From e43816c6007ac3eed30d5e400f4889f278976218 Mon Sep 17 00:00:00 2001
From: delphidabbler <5164283+delphidabbler@users.noreply.github.com>
Date: Wed, 5 Apr 2023 00:27:11 +0100
Subject: [PATCH 023/189] Fix out of range bug in UEncodings unit
Bug in 1st WideCharToMultiByte call in WideCharToChar function.
Fixes #97
---
Src/UEncodings.pas | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Src/UEncodings.pas b/Src/UEncodings.pas
index eb1586899..f33dad412 100644
--- a/Src/UEncodings.pas
+++ b/Src/UEncodings.pas
@@ -439,7 +439,7 @@ function WideCharToChar(const Source: WideChar; const CodePage: Integer;
BufSize: Integer;
begin
BufSize := WideCharToMultiByte(
- CodePage, 0, @Source, 1, @Dest[0], 0, nil, nil
+ CodePage, 0, @Source, 1, nil, 0, nil, nil
);
SetLength(Dest, BufSize + 1);
if WideCharToMultiByte(
From 4111d58910abf10d41189d4fb54bdc051fbde3d6 Mon Sep 17 00:00:00 2001
From: delphidabbler <5164283+delphidabbler@users.noreply.github.com>
Date: Wed, 21 Dec 2022 15:36:27 +0000
Subject: [PATCH 024/189] Add support for rendering REML lists as plain text
ActiveText.UTextRenderer has support added to parsing and rendering REML
lists and to format the resulting text with given margins and page
widths. This was provided in a new TActiveTextTextRenderer.RenderWrapped
method.
TActiveTextTextRenderer.Render was made private since it is now only
called internally.
USourceGen & UTextSnippetDoc were modified to use the above new method.
** This solution is a little kludgy and relies on parsing NBSP
characters emitted by TActiveTextTextRenderer.Render. This was done
originally because the formatting was being done in USourceGen and
UTextSnippetDoc which didn't have access to the inner workings of
TActiveTextTextRenderer. The formatting code was then moved into
TActiveTextTextRenderer and so there's probably a more elegant
solutuion available now.
Generalised TActiveTextTextRenderer.CanEmitInline to determine which
elements can contain text by calling TActiveTextElemCaps rather than
hard wiring the elements.
---
Src/ActiveText.UTextRenderer.pas | 289 +++++++++++++++++++++++++++++--
Src/USourceGen.pas | 86 +++++----
Src/UTextSnippetDoc.pas | 36 ++--
3 files changed, 326 insertions(+), 85 deletions(-)
diff --git a/Src/ActiveText.UTextRenderer.pas b/Src/ActiveText.UTextRenderer.pas
index bd658666e..c060188e6 100644
--- a/Src/ActiveText.UTextRenderer.pas
+++ b/Src/ActiveText.UTextRenderer.pas
@@ -3,7 +3,7 @@
* v. 2.0. If a copy of the MPL was not distributed with this file, You can
* obtain one at https://mozilla.org/MPL/2.0/
*
- * Copyright (C) 2012-2021, Peter Johnson (gravatar.com/delphidabbler).
+ * Copyright (C) 2012-2022, Peter Johnson (gravatar.com/delphidabbler).
*
* Implements class that renders active text as plain text in fixed width, word
* wrapped paragraphs.
@@ -15,17 +15,53 @@
interface
uses
- SysUtils,
- ActiveText.UMain;
+ SysUtils, Generics.Collections,
+ ActiveText.UMain,
+ UConsts;
type
TActiveTextTextRenderer = class(TObject)
+ public
+ const
+ /// Special space character used to indicate the start of a list
+ /// item.
+ /// This special character is a necessary kludge because some
+ /// c odethat renders active text as formatted plain text strips away
+ /// leading #32 characters as part of the formatting process. Therefore
+ /// indentation in list items is lost if #32 characters are used for it.
+ /// NBSP was chosen since it should render the same as a space if calling
+ /// code doesn't convert it.
+ LISpacer = NBSP; // Do not localise. Must be <> #32
+ /// Bullet character used when rendering unordered list items.
+ ///
+ Bullet = '*'; // Do not localise. Must be <> #32 and <> LISpacer
strict private
+ const
+ IndentDelta = 2;
+ type
+ TListKind = (lkNumber, lkBullet);
+ TListState = record
+ public
+ ListNumber: Cardinal;
+ ListKind: TListKind;
+ constructor Create(AListKind: TListKind);
+ end;
+ TLIState = record
+ IsFirstPara: Boolean;
+ constructor Create(AIsFirstPara: Boolean);
+ end;
var
fDisplayURLs: Boolean;
- fInBlock: Boolean;
fParaBuilder: TStringBuilder;
fDocBuilder: TStringBuilder;
+ fBlocksStack: TStack;
+ fListStack: TStack;
+ fLIStack: TStack;
+ fIndent: UInt16;
+ fInPara: Boolean;
+ fInListItem: Boolean;
+ function CanEmitInline: Boolean;
+ procedure AppendToPara(const AText: string);
procedure InitialiseRender;
procedure FinaliseRender;
procedure OutputParagraph;
@@ -33,32 +69,66 @@ TActiveTextTextRenderer = class(TObject)
procedure RenderBlockActionElem(Elem: IActiveTextActionElem);
procedure RenderInlineActionElem(Elem: IActiveTextActionElem);
procedure RenderURL(Elem: IActiveTextActionElem);
+ function Render(ActiveText: IActiveText): string;
public
constructor Create;
destructor Destroy; override;
property DisplayURLs: Boolean read fDisplayURLs write fDisplayURLs
default False;
- function Render(ActiveText: IActiveText): string;
+ function RenderWrapped(ActiveText: IActiveText; const PageWidth, LMargin,
+ ParaOffset: Cardinal; const Prefix: string = '';
+ const Suffix: string = ''): string;
end;
implementation
uses
+ // Delphi
+ Character,
+ // Project
+ UIStringList,
UStrUtils;
{ TActiveTextTextRenderer }
+procedure TActiveTextTextRenderer.AppendToPara(const AText: string);
+begin
+ if AText = '' then
+ Exit;
+ fParaBuilder.Append(AText);
+ fInPara := True;
+end;
+
+function TActiveTextTextRenderer.CanEmitInline: Boolean;
+begin
+ if fBlocksStack.Count <= 0 then
+ Exit(False);
+ Result := TActiveTextElemCaps.CanContainText(fBlocksStack.Peek);
+end;
+
constructor TActiveTextTextRenderer.Create;
begin
+ Assert(LISpacer <> ' ', ClassName + '.Create: LISpacer can''t be #32');
+ Assert(Bullet <> ' ', ClassName + '.Create: Bullet can''t be #32');
+ Assert(Bullet <> LISpacer, ClassName + '.Create: Bullet = LISpacer');
inherited Create;
fParaBuilder := TStringBuilder.Create;
fDocBuilder := TStringBuilder.Create;
fDisplayURLs := False;
+ fBlocksStack := TStack.Create;
+ fListStack := TStack.Create;
+ fLIStack := TStack.Create;
+ fIndent := 0;
+ fInPara := False;
+ fInListItem := False;
end;
destructor TActiveTextTextRenderer.Destroy;
begin
+ fLIStack.Free;
+ fListStack.Free;
+ fBlocksStack.Free;
fDocBuilder.Free;
fParaBuilder.Free;
inherited;
@@ -76,11 +146,33 @@ procedure TActiveTextTextRenderer.InitialiseRender;
end;
procedure TActiveTextTextRenderer.OutputParagraph;
+var
+ LIState: TLIState;
begin
if fParaBuilder.Length = 0 then
Exit;
- fDocBuilder.AppendLine(StrTrim(fParaBuilder.ToString));
+ fDocBuilder.Append(StrOfChar(NBSP, fIndent));
+ if fInListItem and not fLIStack.Peek.IsFirstPara then
+ // Do we need fInListItem? - test for non-empty list stack?
+ // if we do need it, put it on list stack
+ fDocBuilder.Append(StrOfChar(NBSP, IndentDelta));
+ if fLIStack.Count > 0 then
+ begin
+ if not fLIStack.Peek.IsFirstPara then
+ begin
+ fDocBuilder.Append(StrOfChar(NBSP, IndentDelta));
+ end
+ else
+ begin
+ // Update item at top of stack
+ LIState := fLIStack.Pop;
+ LIState.IsFirstPara := False;
+ fLIStack.Push(LIState);
+ end;
+ end;
+ fDocBuilder.AppendLine(StrTrimRight(fParaBuilder.ToString));
fParaBuilder.Clear;
+ fInPara := False;
end;
function TActiveTextTextRenderer.Render(ActiveText: IActiveText): string;
@@ -90,7 +182,6 @@ function TActiveTextTextRenderer.Render(ActiveText: IActiveText): string;
ActionElem: IActiveTextActionElem;
begin
InitialiseRender;
- fInBlock := False;
for Elem in ActiveText do
begin
if Supports(Elem, IActiveTextTextElem, TextElem) then
@@ -109,16 +200,82 @@ function TActiveTextTextRenderer.Render(ActiveText: IActiveText): string;
procedure TActiveTextTextRenderer.RenderBlockActionElem(
Elem: IActiveTextActionElem);
+var
+ ListState: TListState;
begin
case Elem.State of
fsOpen:
begin
- fInBlock := True;
+ fBlocksStack.Push(Elem.Kind);
+ case Elem.Kind of
+ ekPara: {Do nothing} ;
+ ekHeading: {Do nothing} ;
+ ekUnorderedList:
+ begin
+ if (fListStack.Count > 0) and (fInPara) then
+ OutputParagraph;
+ fListStack.Push(TListState.Create(lkBullet));
+ Inc(fIndent, IndentDelta);
+ end;
+ ekOrderedList:
+ begin
+ if (fListStack.Count > 0) and (fInPara) then
+ OutputParagraph;
+ fListStack.Push(TListState.Create(lkNumber));
+ Inc(fIndent, IndentDelta);
+ end;
+ ekListItem:
+ begin
+ // Update list number of current list
+ ListState := fListStack.Pop;
+ Inc(ListState.ListNumber, 1);
+ fListStack.Push(ListState);
+ // Push this list item to list item stack
+ fLIStack.Push(TLIState.Create(True));
+ // Act depending on current list kind
+ case fListStack.Peek.ListKind of
+ lkNumber:
+ begin
+ // Number list: start a new numbered item, with current number
+ fParaBuilder.Append(IntToStr(fListStack.Peek.ListNumber));
+ fParaBuilder.Append(NBSP);
+ end;
+ lkBullet:
+ begin
+ // Bullet list: start a new bullet point
+ fParaBuilder.Append(Bullet + NBSP);
+ end;
+ end;
+ end;
+ end;
end;
fsClose:
begin
- OutputParagraph;
- fInBlock := False;
+ case Elem.Kind of
+ ekPara:
+ OutputParagraph;
+ ekHeading:
+ OutputParagraph;
+ ekUnorderedList:
+ begin
+ OutputParagraph;
+ fListStack.Pop;
+ Dec(fIndent, IndentDelta);
+ end;
+ ekOrderedList:
+ begin
+ OutputParagraph;
+ fListStack.Pop;
+ Dec(fIndent, IndentDelta);
+ end;
+ ekListItem:
+ begin
+ OutputParagraph;
+ fInListItem := False;
+ fLIStack.Pop;
+ end;
+ end;
+ fBlocksStack.Pop;
end;
end;
end;
@@ -126,17 +283,27 @@ procedure TActiveTextTextRenderer.RenderBlockActionElem(
procedure TActiveTextTextRenderer.RenderInlineActionElem(
Elem: IActiveTextActionElem);
begin
- if not fInBlock then
+ if not CanEmitInline then
Exit;
if (Elem.Kind = ekLink) and (Elem.State = fsClose) and fDisplayURLs then
RenderURL(Elem);
+ // else ignore element: formatting elements have no effect on plain text
end;
procedure TActiveTextTextRenderer.RenderTextElem(Elem: IActiveTextTextElem);
+var
+ TheText: string;
begin
- if not fInBlock then
+ if not CanEmitInline then
Exit;
- fParaBuilder.Append(Elem.Text);
+ TheText := Elem.Text;
+ // no white space emitted after block start until 1st non-white space
+ // character encountered
+ if not fInPara then
+ TheText := StrTrimLeft(Elem.Text);
+ if TheText = '' then
+ Exit;
+ AppendToPara(TheText);
end;
procedure TActiveTextTextRenderer.RenderURL(Elem: IActiveTextActionElem);
@@ -144,7 +311,101 @@ procedure TActiveTextTextRenderer.RenderURL(Elem: IActiveTextActionElem);
sURL = ' (%s)'; // formatting for URLs from hyperlinks
begin
Assert(Elem.Kind = ekLink, ClassName + '.RenderURL: Not a link element');
- fParaBuilder.AppendFormat(sURL, [Elem.Attrs[TActiveTextAttrNames.Link_URL]]);
+ AppendToPara(Format(sURL, [Elem.Attrs[TActiveTextAttrNames.Link_URL]]));
+end;
+
+function TActiveTextTextRenderer.RenderWrapped(ActiveText: IActiveText;
+ const PageWidth, LMargin, ParaOffset: Cardinal; const Prefix, Suffix: string):
+ string;
+var
+ Paras: IStringList;
+ Para: string;
+ ParaIndent: UInt16;
+ WrappedPara: string;
+ Offset: Int16;
+
+ // Calculate indent of paragraph by counting LISpacer characters inserted by
+ // Render method
+ function CalcParaIndent: UInt16;
+ var
+ Ch: Char;
+ begin
+ Result := 0;
+ for Ch in Para do
+ begin
+ if Ch <> LISpacer then
+ Break;
+ Inc(Result);
+ end;
+ end;
+
+ // Calculate if we are currently processing a list item by detecting Bullet,
+ // digits and LISpacer characters inserted by Render method
+ function IsListItem: Boolean;
+ var
+ Remainder: string;
+ Digits: string;
+ Ch: Char;
+ begin
+ Result := False;
+ // Strip any leading spacer chars from start of para
+ Remainder := StrTrimLeftChars(Para, LISpacer);
+ // Check for bullet list: starts with bullet character then spacer
+ if StrStartsStr(Bullet + LISpacer, Remainder) then
+ Exit(True);
+ // Check for number list: starts with digit(s) then spacer
+ Digits := '';
+ for Ch in Remainder do
+ if TCharacter.IsDigit(Ch) then
+ Digits := Digits + Ch
+ else
+ Break;
+ if (Digits <> '') and
+ StrStartsStr(Digits + LISpacer, Remainder) then
+ Exit(True);
+ end;
+
+begin
+ Result := '';
+ Paras := TIStringList.Create(Prefix + Render(ActiveText) + Suffix, EOL, True);
+ for Para in Paras do
+ begin
+ if IsListItem then
+ begin
+ Offset := -ParaOffset;
+ ParaIndent := CalcParaIndent + LMargin + ParaOffset;
+ end
+ else
+ begin
+ Offset := 0;
+ ParaIndent := CalcParaIndent + LMargin;
+ end;
+ WrappedPara := StrWrap(
+ StrReplace(Para, LISpacer, ' '),
+ PageWidth - ParaIndent,
+ ParaIndent,
+ Offset
+ );
+ if Result <> '' then
+ Result := Result + EOL;
+ Result := Result + StrTrimRight(WrappedPara);
+ end;
+ Result := StrTrimRight(Result);
+end;
+
+{ TActiveTextTextRenderer.TListState }
+
+constructor TActiveTextTextRenderer.TListState.Create(AListKind: TListKind);
+begin
+ ListNumber := 0;
+ ListKind := AListKind;
+end;
+
+{ TActiveTextTextRenderer.TLIState }
+
+constructor TActiveTextTextRenderer.TLIState.Create(AIsFirstPara: Boolean);
+begin
+ IsFirstPara := AIsFirstPara;
end;
end.
diff --git a/Src/USourceGen.pas b/Src/USourceGen.pas
index a8c5f1cd7..3d10b57ef 100644
--- a/Src/USourceGen.pas
+++ b/Src/USourceGen.pas
@@ -3,7 +3,7 @@
* v. 2.0. If a copy of the MPL was not distributed with this file, You can
* obtain one at https://mozilla.org/MPL/2.0/
*
- * Copyright (C) 2005-2021, Peter Johnson (gravatar.com/delphidabbler).
+ * Copyright (C) 2005-2022, Peter Johnson (gravatar.com/delphidabbler).
*
* Implements a class that is used to generate Pascal source code containing
* specified database snippets.
@@ -38,10 +38,10 @@ interface
TSourceComments = class(TNoConstructObject)
strict private
/// Formats the given comment text into lines with a fixed
- /// maximum width indented by the given number of spaces on the left.
- ///
- class function FormatCommentLines(const Text: string;
- const Indent: Cardinal): string;
+ /// maximum width indented by the given number of spaces on the left,
+ /// optionally truncated to the first paragraph.
+ class function FormatActiveTextCommentInner(ActiveText: IActiveText;
+ const Indent: Cardinal; const Truncate: Boolean): string;
public
/// Returns a description of the given comment style.
@@ -250,7 +250,7 @@ implementation
uses
// Delphi
- SysUtils,
+ SysUtils, Character,
// Project
ActiveText.UTextRenderer, DB.USnippetKind, UConsts, UExceptions, UPreferences,
USnippetValidator, UStrUtils, UWarnings, Hiliter.UPasLexer;
@@ -272,7 +272,7 @@ TRoutineFormatter = class(TNoConstructObject)
/// Splits source code of a routine snippet into the head (routine
/// prototype) and body.
- /// TSnippet [in] Routine whose source code is to be
+ /// TSnippet [in3] Routine whose source code is to be
/// split.
/// string [out] Set to routine prototype.
/// string [out] Body of routine that follows the
@@ -1136,17 +1136,25 @@ class function TSourceComments.CommentStyleDesc(
Result := sDescriptions[Style];
end;
-class function TSourceComments.FormatCommentLines(const Text: string;
- const Indent: Cardinal): string;
+class function TSourceComments.FormatActiveTextCommentInner(
+ ActiveText: IActiveText; const Indent: Cardinal; const Truncate: Boolean):
+ string;
var
- Lines: TStringList;
+ Renderer: TActiveTextTextRenderer;
+ ProcessedActiveText: IActiveText;
begin
- Lines := TStringList.Create;
+ if Truncate then
+ ProcessedActiveText := ActiveText.FirstBlock
+ else
+ ProcessedActiveText := ActiveText;
+ Renderer := TActiveTextTextRenderer.Create;
try
- Lines.Text := Text;
- Result := StrTrimRight(StrWrap(Lines, cLineWidth - Indent, Indent, False));
+ Renderer.DisplayURLs := False;
+ Result := Renderer.RenderWrapped(
+ ProcessedActiveText, cLineWidth, Indent, Indent
+ );
finally
- Lines.Free;
+ Renderer.Free;
end;
end;
@@ -1156,7 +1164,7 @@ class function TSourceComments.FormatHeaderComments(
Line: string; // loops thru each line of comments & exploded comments
Lines: IStringList; // comments after exploding multiple wrapped lines
const
- cLinePrefix = ' * '; // prefixes each comment line
+ cLinePrefix = ' * '; // prefixes each header omment line
begin
// Only create comment if some comment text is provided
if Assigned(Comments) and (Comments.Count > 0) then
@@ -1182,38 +1190,24 @@ class function TSourceComments.FormatHeaderComments(
class function TSourceComments.FormatSnippetComment(const Style: TCommentStyle;
const TruncateComments: Boolean; const Text: IActiveText): string;
-var
- Renderer: TActiveTextTextRenderer;
- PlainText: string;
- Lines: IStringList;
begin
- Renderer := TActiveTextTextRenderer.Create;
- try
- Renderer.DisplayURLs := False;
- PlainText := Renderer.Render(Text);
- if TruncateComments then
- begin
- // use first non-empty paragraph of Text as comment
- Lines := TIStringList.Create(PlainText, string(sLineBreak), False);
- if Lines.Count > 0 then
- PlainText := Lines[0];
- end;
- case Style of
- csNone:
- Result := '';
- csBefore:
- Result := '{'
- + EOL
- + FormatCommentLines(PlainText, cIndent)
- + EOL
- + '}';
- csAfter:
- Result := FormatCommentLines(
- '{' + PlainText + '}', cIndent
- );
- end;
- finally
- Renderer.Free;
+ case Style of
+ csNone:
+ Result := '';
+ csBefore:
+ Result := '{'
+ + EOL
+ + FormatActiveTextCommentInner(Text, cIndent, TruncateComments)
+ + EOL
+ + '}';
+ csAfter:
+ Result := StrOfChar(TActiveTextTextRenderer.LISpacer, cIndent)
+ + '{'
+ + EOL
+ + FormatActiveTextCommentInner(Text, 2 * cIndent, TruncateComments)
+ + EOL
+ + StrOfChar(TActiveTextTextRenderer.LISpacer, cIndent)
+ + '}';
end;
end;
diff --git a/Src/UTextSnippetDoc.pas b/Src/UTextSnippetDoc.pas
index ad6d1fbf7..87e72c4dd 100644
--- a/Src/UTextSnippetDoc.pas
+++ b/Src/UTextSnippetDoc.pas
@@ -3,7 +3,7 @@
* v. 2.0. If a copy of the MPL was not distributed with this file, You can
* obtain one at https://mozilla.org/MPL/2.0/
*
- * Copyright (C) 2009-2021, Peter Johnson (gravatar.com/delphidabbler).
+ * Copyright (C) 2009-2022, Peter Johnson (gravatar.com/delphidabbler).
*
* Implements a class that renders a document that describes a snippet as plain
* text.
@@ -39,10 +39,8 @@ TTextSnippetDoc = class(TSnippetDoc)
cIndent = 2;
strict private
/// Renders given active text as word-wrapped paragraphs of width
- /// cPageWidth and given indent. Blank lines are added between paragraphs
- /// iff SpaceParas in True.
- procedure RenderActiveText(ActiveText: IActiveText; const Indent: Cardinal;
- const SpaceParas: Boolean);
+ /// cPageWidth.
+ procedure RenderActiveText(ActiveText: IActiveText);
strict protected
/// Initialises plain text document.
procedure InitialiseDoc; override;
@@ -88,9 +86,9 @@ implementation
uses
// Delphi
- SysUtils,
+ SysUtils, Character,
// Project
- ActiveText.UTextRenderer, UStrUtils;
+ ActiveText.UTextRenderer, UConsts, UStrUtils;
{ TTextSnippetDoc }
@@ -106,28 +104,16 @@ procedure TTextSnippetDoc.InitialiseDoc;
fWriter := TStringWriter.Create;
end;
-procedure TTextSnippetDoc.RenderActiveText(ActiveText: IActiveText;
- const Indent: Cardinal; const SpaceParas: Boolean);
+procedure TTextSnippetDoc.RenderActiveText(ActiveText: IActiveText);
var
Renderer: TActiveTextTextRenderer;
- Lines: TStringList;
begin
Renderer := TActiveTextTextRenderer.Create;
try
Renderer.DisplayURLs := True;
- Lines := TStringList.Create;
- try
- Lines.Text := Renderer.Render(ActiveText);
- fWriter.WriteLine(
- StrTrimRight(
- StrWrap(
- Lines, cPageWidth - Indent, Indent, True
- )
- )
- );
- finally
- Lines.Free;
- end;
+ fWriter.WriteLine(
+ Renderer.RenderWrapped(ActiveText, cPageWidth, 0, cIndent)
+ );
finally
Renderer.Free;
end;
@@ -153,14 +139,14 @@ procedure TTextSnippetDoc.RenderDBInfo(const Text: string);
procedure TTextSnippetDoc.RenderDescription(const Desc: IActiveText);
begin
fWriter.WriteLine;
- RenderActiveText(Desc, 0, True);
+ RenderActiveText(Desc);
end;
procedure TTextSnippetDoc.RenderExtra(const ExtraText: IActiveText);
begin
Assert(not ExtraText.IsEmpty, ClassName + '.RenderExtra: ExtraText is empty');
fWriter.WriteLine;
- RenderActiveText(ExtraText, 0, True);
+ RenderActiveText(ExtraText);
end;
procedure TTextSnippetDoc.RenderHeading(const Heading: string;
From 36bc98384821240ebe182bd5d9dbaa3aacd5fb2f Mon Sep 17 00:00:00 2001
From: delphidabbler <5164283+delphidabbler@users.noreply.github.com>
Date: Wed, 21 Dec 2022 16:00:50 +0000
Subject: [PATCH 025/189] Missing text at end of detail pane list items fixed
Code added to track and detect if text is being written inside a list
item and to output any inline text as HTML between the end of a nested
list and the end of a list item.
Such detection is then used in logic that determines when to output such
orphaned text.
Fixes #82
---
Src/ActiveText.UHTMLRenderer.pas | 21 +++++++++++----------
1 file changed, 11 insertions(+), 10 deletions(-)
diff --git a/Src/ActiveText.UHTMLRenderer.pas b/Src/ActiveText.UHTMLRenderer.pas
index daa25dc0d..05e3f862f 100644
--- a/Src/ActiveText.UHTMLRenderer.pas
+++ b/Src/ActiveText.UHTMLRenderer.pas
@@ -3,7 +3,7 @@
* v. 2.0. If a copy of the MPL was not distributed with this file, You can
* obtain one at https://mozilla.org/MPL/2.0/
*
- * Copyright (C) 2009-2021, Peter Johnson (gravatar.com/delphidabbler).
+ * Copyright (C) 2009-2022, Peter Johnson (gravatar.com/delphidabbler).
*
* Provides a class that renders active text as HTML.
}
@@ -17,9 +17,9 @@ interface
uses
// Delphi
- SysUtils, Graphics, Generics.Collections,
+ SysUtils,
// Project
- ActiveText.UMain, UBaseObjects, UCSSBuilder, UHTMLUtils;
+ ActiveText.UMain, UHTMLUtils;
type
@@ -65,6 +65,7 @@ TCSSStyles = class(TObject)
fBuilder: TStringBuilder;
fInBlock: Boolean;
fTagInfoMap: TTagInfoMap;
+ fLINestingDepth: Cardinal;
procedure InitialiseTagInfoMap;
procedure InitialiseRender;
procedure RenderTextElem(Elem: IActiveTextTextElem);
@@ -84,11 +85,6 @@ TCSSStyles = class(TObject)
implementation
-uses
- // Project
- UColours, UCSSUtils, UFontHelper, UIStringList;
-
-
{ TActiveTextHTML }
constructor TActiveTextHTML.Create;
@@ -96,6 +92,7 @@ constructor TActiveTextHTML.Create;
inherited Create;
fCSSStyles := TCSSStyles.Create;
fBuilder := TStringBuilder.Create;
+ fLINestingDepth := 0;
InitialiseTagInfoMap;
end;
@@ -209,6 +206,8 @@ procedure TActiveTextHTML.RenderBlockActionElem(Elem: IActiveTextActionElem);
case Elem.State of
fsOpen:
begin
+ if Elem.Kind = ekListItem then
+ Inc(fLINestingDepth);
fBuilder.Append(MakeOpeningTag(Elem));
fInBlock := True;
end;
@@ -216,13 +215,15 @@ procedure TActiveTextHTML.RenderBlockActionElem(Elem: IActiveTextActionElem);
begin
fInBlock := False;
fBuilder.AppendLine(MakeClosingTag(Elem));
+ if Elem.Kind = ekListItem then
+ Dec(fLINestingDepth);
end;
end;
end;
procedure TActiveTextHTML.RenderInlineActionElem(Elem: IActiveTextActionElem);
begin
- if not fInBlock then
+ if not fInBlock and (fLINestingDepth = 0) then
Exit;
case Elem.State of
fsOpen:
@@ -234,7 +235,7 @@ procedure TActiveTextHTML.RenderInlineActionElem(Elem: IActiveTextActionElem);
procedure TActiveTextHTML.RenderTextElem(Elem: IActiveTextTextElem);
begin
- if not fInBlock then
+ if not fInBlock and (fLINestingDepth = 0) then
Exit;
fBuilder.Append(THTML.Entities(Elem.Text));
end;
From 02e85186bdc8c233c9c083a967da2ac7f5f16d38 Mon Sep 17 00:00:00 2001
From: delphidabbler <5164283+delphidabbler@users.noreply.github.com>
Date: Thu, 6 Apr 2023 02:31:57 +0100
Subject: [PATCH 026/189] Fix active text editor re revised active text docs
Modified code that converts from plain text to active text when user
switched from editing plain text to editing active text. The active text
document that is created from plain text is now wrapped in ekDocument
elements.
---
Src/FmSnippetsEditorDlg.FrActiveTextEditor.pas | 2 ++
1 file changed, 2 insertions(+)
diff --git a/Src/FmSnippetsEditorDlg.FrActiveTextEditor.pas b/Src/FmSnippetsEditorDlg.FrActiveTextEditor.pas
index 541f19416..13282fa4a 100644
--- a/Src/FmSnippetsEditorDlg.FrActiveTextEditor.pas
+++ b/Src/FmSnippetsEditorDlg.FrActiveTextEditor.pas
@@ -222,6 +222,7 @@ function TSnippetsActiveTextEdFrame.PlainTextToActiveText(Text: string):
if Text = '' then
Exit;
Paragraphs := TIStringList.Create(Text, EOL2, False, True);
+ Result.AddElem(TActiveTextFactory.CreateActionElem(ekDocument, fsOpen));
for Paragraph in Paragraphs do
begin
Result.AddElem(TActiveTextFactory.CreateActionElem(ekPara, fsOpen));
@@ -230,6 +231,7 @@ function TSnippetsActiveTextEdFrame.PlainTextToActiveText(Text: string):
);
Result.AddElem(TActiveTextFactory.CreateActionElem(ekPara, fsClose));
end;
+ Result.AddElem(TActiveTextFactory.CreateActionElem(ekDocument, fsClose));
end;
procedure TSnippetsActiveTextEdFrame.Preview;
From 27155851e28dc52a0566d1da8e2fdcfc33dcdbc8 Mon Sep 17 00:00:00 2001
From: delphidabbler <5164283+delphidabbler@users.noreply.github.com>
Date: Thu, 6 Apr 2023 02:35:56 +0100
Subject: [PATCH 027/189] Heavily overhaul REML writer and parser.
Major changes to REML parser to:
1. correctly deal with plain text outside of any REML blocks -> such
text is now included in ekBlock blocks.
2. create active text that conforms to the revised document structure
and rules.
REML Writer revised to handle new ekDocument and ekBlock active text
elements. Neither element has any equivalent REML tag. ekDocument is
simply ignored, while active text contained in ekBlock elements is
written out with no surrounding block tag.
---
Src/UREMLDataIO.pas | 626 +++++++++++++++++++++++++++++---------------
1 file changed, 417 insertions(+), 209 deletions(-)
diff --git a/Src/UREMLDataIO.pas b/Src/UREMLDataIO.pas
index c6ae83bbc..526369a89 100644
--- a/Src/UREMLDataIO.pas
+++ b/Src/UREMLDataIO.pas
@@ -34,11 +34,28 @@ interface
}
TREMLReader = class(TInterfacedObject, IActiveTextParser)
strict private
- fLexer: TTaggedTextLexer; // Analyses REML markup
- // Stack of tag params for use in closing tags
- fParamStack: TStack;
- // Stack of block level tags
- fBlockTagStack: TStack;
+ type
+ TBlockTagInfo = record
+ strict private
+ var
+ fTag: TActiveTextActionElemKind;
+ fHasContent: Boolean;
+ public
+ constructor Create(ATag: TActiveTextActionElemKind);
+ property Tag: TActiveTextActionElemKind read fTag;
+ property HasContent: Boolean read fHasContent write fHasContent;
+ end;
+ var
+ fLexer: TTaggedTextLexer; // Analyses REML markup
+ // Stack of tag params for use in closing tags
+ fParamStack: TStack;
+ // Stack of block level tags with record of whether block has yet got any
+ // content.
+ fBlockTagStack: TStack;
+ // Stack of all compound tags
+ fTagStack: TStack;
+ // Flag indicating whether parsing an implied block
+ fIsImpliedBlock: Boolean;
function TagInfo(const TagIdx: Integer; out TagName: string;
out TagCode: Word; out IsContainer: Boolean): Boolean;
{Callback that provides lexer with information about supported tags. Lexer
@@ -61,6 +78,27 @@ TREMLReader = class(TInterfacedObject, IActiveTextParser)
@return True if entity information was provided or False to indicate no
more entities.
}
+ /// Note that block tag at top of stack has had content written to
+ /// it.
+ procedure NoteBlockTagHasContent;
+ procedure StartElem(const AActiveText: IActiveText;
+ const AElem: TActiveTextActionElemKind);
+ procedure EndElem(const AActiveText: IActiveText;
+ const AElem: TActiveTextActionElemKind);
+ procedure StartInlineElem(const AActiveText: IActiveText;
+ const AElem: TActiveTextActionElemKind);
+ procedure EndInlineElem(const AActiveText: IActiveText;
+ const AElem: TActiveTextActionElemKind);
+ procedure StartBlockElem(const AActiveText: IActiveText;
+ const AElem: TActiveTextActionElemKind);
+ procedure EndBlockElem(const AActiveText: IActiveText;
+ const AElem: TActiveTextActionElemKind);
+ procedure StartImpliedBlockElem(const AActiveText: IActiveText);
+ procedure EndImpliedBlockElem(const AActiveText: IActiveText);
+ procedure WriteText(const AActiveText: IActiveText; const AText: string);
+ procedure ParsePlainText(const AActiveText: IActiveText);
+ procedure ParseStartTag(const AActiveText: IActiveText);
+ procedure ParseEndTag(const AActiveText: IActiveText);
public
constructor Create;
{Class constructor. Sets up object.
@@ -69,12 +107,12 @@ TREMLReader = class(TInterfacedObject, IActiveTextParser)
{Class destructor. Finalises object.
}
{ IActiveTextParser method }
- procedure Parse(const Markup: string; const ActiveText: IActiveText);
{Parses markup and updates active text object with details.
@param Markup [in] Markup containing definition of active text. Must be
in format understood by parser.
@param ActiveText [in] Active text object updated by parser.
}
+ procedure Parse(const Markup: string; const ActiveText: IActiveText);
end;
{
@@ -295,7 +333,8 @@ constructor TREMLReader.Create;
inherited Create;
fLexer := TTaggedTextLexer.Create(TagInfo, EntityInfo);
fParamStack := TStack.Create;
- fBlockTagStack := TStack.Create;
+ fTagStack := TStack.Create;
+ fBlockTagStack := TStack.Create;
end;
destructor TREMLReader.Destroy;
@@ -303,11 +342,80 @@ destructor TREMLReader.Destroy;
}
begin
fBlockTagStack.Free;
+ fTagStack.Free;
FreeAndNil(fParamStack);
FreeAndNil(fLexer);
inherited;
end;
+procedure TREMLReader.EndBlockElem(const AActiveText: IActiveText;
+ const AElem: TActiveTextActionElemKind);
+resourcestring
+ // Error message
+ sMismatchedTag = 'Closing block tag does not match opening block tag.';
+begin
+ Assert(fBlockTagStack.Count > 0);
+ if fBlockTagStack.Peek.Tag <> AElem then
+ raise EActiveTextParserError.Create(sMismatchedTag);
+ EndElem(AActiveText, AElem);
+ fBlockTagStack.Pop;
+end;
+
+procedure TREMLReader.EndElem(const AActiveText: IActiveText;
+ const AElem: TActiveTextActionElemKind);
+var
+ ParamName: string; // name of a parameter
+ Attr: TActiveTextAttr; // attributes of tag
+resourcestring
+ // Error message
+ sMismatchedTag = 'Closing tag does not match opening tag.';
+begin
+ Assert(fTagStack.Count > 0);
+ if AElem <> fTagStack.Peek then
+ raise EActiveTextParserError.Create(sMismatchedTag);
+ // Retrive any parameters and record element with any parameter
+ TREMLTags.LookupParamName(AElem, ParamName);
+ if ParamName <> '' then
+ begin
+ // We should have a param which must be stored in closing action
+ // element, but closing REML tags have no parameters. We solve this
+ // by popping the parameter value from the stack. This works because
+ // we use a stack for params and opening and closing tags are
+ // matched.
+ Assert(fParamStack.Count > 0);
+ Attr := fParamStack.Pop;
+ // Add closing action element
+ AActiveText.AddElem(
+ TActiveTextFactory.CreateActionElem(
+ AElem, TActiveTextFactory.CreateAttrs(Attr), fsClose
+ )
+ );
+ end
+ else
+ begin
+ // No parameter: simple add closing parameterless action element
+ AActiveText.AddElem(
+ TActiveTextFactory.CreateActionElem(AElem, fsClose)
+ );
+ end;
+ // Pop tag from tag stack
+ fTagStack.Pop;
+end;
+
+procedure TREMLReader.EndImpliedBlockElem(const AActiveText: IActiveText);
+begin
+ Assert(fIsImpliedBlock);
+ Assert(fBlockTagStack.Peek.Tag = ekBlock);
+ fIsImpliedBlock := False;
+ EndBlockElem(AActiveText, ekBlock);
+end;
+
+procedure TREMLReader.EndInlineElem(const AActiveText: IActiveText;
+ const AElem: TActiveTextActionElemKind);
+begin
+ EndElem(AActiveText, AElem);
+end;
+
function TREMLReader.EntityInfo(const EntityIdx: Integer;
out EntityName: string; out EntityChar: Char): Boolean;
{Callback that provides lexer with information about supported character
@@ -326,176 +434,62 @@ function TREMLReader.EntityInfo(const EntityIdx: Integer;
EntityChar := TREMLEntities.Chars[EntityIdx];
end;
-procedure TREMLReader.Parse(const Markup: string;
- const ActiveText: IActiveText);
- {Parses markup and updates active text object with details.
- @param Markup [in] Markup containing definition of active text. Must be in
- format understood by parser.
- @param ActiveText [in] Active text object updated by parser.
- }
+procedure TREMLReader.NoteBlockTagHasContent;
var
- ParamName: string; // name of a parameter
- ParamValue: string; // value of a parameter
- TagId: TActiveTextActionElemKind; // id of a tag
- Attr: TActiveTextAttr; // attributes of tag
-
- function IsTextPermittedInParentBlock: Boolean;
- begin
- if fBlockTagStack.Count = 0 then
- Exit(True);
- Result := TActiveTextElemCaps.CanContainText(fBlockTagStack.Peek);
- end;
-
- function IsElemPermittedParentBlock(const Elem: TActiveTextActionElemKind):
- Boolean;
- begin
- if fBlockTagStack.Count = 0 then
- Exit(TActiveTextElemCaps.IsElemPermittedInRoot(Elem));
- Result := TActiveTextElemCaps.IsRequiredParent(fBlockTagStack.Peek, Elem);
- end;
-
- function IsElemExcluded(const Elem: TActiveTextActionElemKind):
- Boolean;
- begin
- if fBlockTagStack.Count = 0 then
- Exit(False);
- Result := TActiveTextElemCaps.IsExcludedElem(fBlockTagStack.Peek, Elem);
- end;
+ Block: TBlockTagInfo;
+begin
+ Assert(fBlockTagStack.Count > 0);
+ if fBlockTagStack.Peek.HasContent then
+ Exit;
+ Block := fBlockTagStack.Pop;
+ Block.HasContent := True;
+ fBlockTagStack.Push(Block);
+end;
+procedure TREMLReader.Parse(const Markup: string;
+ const ActiveText: IActiveText);
resourcestring
- // Error message
- sErrMissingParam = 'Expected a "%0:s" parameter value in tag "%1:s"';
- sErrNesting = 'Illegal nesting of "%0:s" tag';
- sBadParentBlock = 'Invalid parent block for tag %0:s';
- sNoTextPermitted = 'Text is not permitted in enclosing block';
- sMismatchedCloser = 'Mismatching closing block tag %0:s';
+ sMismatchedTags = 'There is not a closing tag for each opening tag';
+ sBadTagType = 'Unexpected tag type';
begin
Assert(Assigned(ActiveText), ClassName + '.Parse: ActiveText is nil');
+
+ // TODO: consider changing this so document tags are written
+ if Markup = '' then
+ Exit;
+
fBlockTagStack.Clear;
+ fTagStack.Clear;
+ fParamStack.Clear;
+ fIsImpliedBlock := False;
+
try
- // Nothing to do if there is no markup
- if Markup = '' then
- Exit;
// Use lexer to process markup
fLexer.TaggedText := Markup;
- // Scan REML a token at a time
+
+ StartBlockElem(ActiveText, ekDocument);
+
while fLexer.NextItem <> ttsEOF do
begin
case fLexer.Kind of
-
ttsText:
- begin
- if IsTextPermittedInParentBlock then
- begin
- // Plain text is allowed in parent block: add it
- ActiveText.AddElem(
- TActiveTextFactory.CreateTextElem(fLexer.PlainText)
- );
- end
- else
- begin
- // Plain text not allowed in parent block: raise exception UNLESS
- // text is only white space or empty, in which case we simply ignore
- // the text. This is because white space will often occur after
- // end tag of enclosed blocks
- if not StrIsEmpty(fLexer.PlainText, True) then
- raise EActiveTextParserError.Create(sNoTextPermitted);
- end
- end;
-
+ ParsePlainText(ActiveText);
ttsCompoundStartTag:
- begin
- // Start of an action element
- // Get tag id and any parameter
- TagId := TActiveTextActionElemKind(fLexer.TagCode);
-
- // Validate tag id
- if IsElemExcluded(TagId) then
- raise EActiveTextParserError.CreateFmt(
- sErrNesting, [fLexer.TagName]
- );
- if not IsElemPermittedParentBlock(TagID) then
- raise EActiveTextParserError.CreateFmt(
- sBadParentBlock, [fLexer.TagName]
- );
-
- if TActiveTextElemCaps.DisplayStyleOf(TagId) = dsBlock then
- fBlockTagStack.Push(TagId);
- TREMLTags.LookupParamName(TagId, ParamName);
- if ParamName <> '' then
- begin
- // We have a parameter: must not be empty
- ParamValue := fLexer.TagParams.Values[ParamName];
- if ParamValue = '' then
- raise EActiveTextParserError.CreateFmt(
- sErrMissingParam, [ParamName, fLexer.TagName]
- );
- // Record param for use by closing tag
- Attr := TActiveTextAttr.Create(ParamName, ParamValue);
- fParamStack.Push(Attr);
- // Add opening action element
- ActiveText.AddElem(
- TActiveTextFactory.CreateActionElem(
- TagId, TActiveTextFactory.CreateAttrs(Attr), fsOpen
- )
- );
- end
- else
- begin
- // No parameter: simply add opening parameterless action element
- ActiveText.AddElem(
- TActiveTextFactory.CreateActionElem(TagId, fsOpen)
- );
- end;
- end;
-
+ ParseStartTag(ActiveText);
ttsCompoundEndTag:
- begin
- // End of an action element
- // Get elem id
- TagId := TActiveTextActionElemKind(fLexer.TagCode);
-
- // Validate elem
- if TActiveTextElemCaps.DisplayStyleOf(TagId) = dsBlock then
- begin
- if fBlockTagStack.Peek <> TagId then
- raise EActiveTextParserError.CreateFmt(
- sMismatchedCloser, [fLexer.TagName]
- );
- fBlockTagStack.Pop;
- end;
-
- // Process params
- TREMLTags.LookupParamName(TagId, ParamName);
- if ParamName <> '' then
- begin
- // We should have a param which must be stored in closing action
- // element, but closing REML tags have no parameters. We solve this
- // by popping the parameter value from the stack. This works because
- // we use a stack for params and opening and closing tags are
- // matched.
- Attr := fParamStack.Pop;
- // Add closing action element
- ActiveText.AddElem(
- TActiveTextFactory.CreateActionElem(
- TagId, TActiveTextFactory.CreateAttrs(Attr), fsClose
- )
- );
- end
- else
- begin
- // No parameter: simple add closing parameterless action element
- ActiveText.AddElem(
- TActiveTextFactory.CreateActionElem(TagId, fsClose)
- );
- end;
- end;
-
+ ParseEndTag(ActiveText);
+ else
+ raise EActiveTextParserError.Create(sBadTagType);
end;
end;
- except
+ if fIsImpliedBlock then
+ EndImpliedBlockElem(ActiveText);
+ EndBlockElem(ActiveText, ekDocument);
+ if fBlockTagStack.Count <> 0 then
+ raise EActiveTextParserError.Create(sMismatchedTags);
+ except
// Handle exceptions: convert expected exceptions to EActiveTextParserError
on E: ETaggedTextLexer do
raise EActiveTextParserError.Create(E);
@@ -504,6 +498,177 @@ procedure TREMLReader.Parse(const Markup: string;
end;
end;
+procedure TREMLReader.ParseEndTag(const AActiveText: IActiveText);
+var
+ TagId: TActiveTextActionElemKind; // id of a tag
+begin
+ Assert(flexer.Kind = ttsCompoundEndTag);
+ // Get tag id
+ TagId := TActiveTextActionElemKind(fLexer.TagCode);
+
+ if TActiveTextElemCaps.DisplayStyleOf(TagId) = dsBlock then
+ begin
+ // Closing block tag
+ if fIsImpliedBlock then
+ // An implied block is being written: close it
+ EndImpliedBlockElem(AActiveText);
+ // End read closing block
+ EndBlockElem(AActiveText, TagId)
+ end
+ else // TActiveTextElemCaps.DisplayStyleOf(TagId) = dsInline
+ // Closing inline tag: just close it
+ EndInlineElem(AActiveText, TagId);
+end;
+
+procedure TREMLReader.ParsePlainText(const AActiveText: IActiveText);
+var
+ Text: string;
+resourcestring
+ sNoTextPermitted = 'Text is not permitted in enclosing block';
+begin
+ Assert(fLexer.Kind = ttsText);
+ Text := fLexer.PlainText;
+ if TActiveTextElemCaps.CanContainText(fBlockTagStack.Peek.Tag) then
+ // Parent block accepts text: write it
+ WriteText(AActiveText, Text)
+ else if TActiveTextElemCaps.IsPermittedChildElem(
+ fBlockTagStack.Peek.Tag, ekBlock
+ ) then
+ begin
+ // Parent block can contain an ekBlock:
+ // create block if text is not strictly empty string, and add text to it
+ if not StrIsEmpty(Text) then
+ begin
+ StartImpliedBlockElem(AActiveText);
+ WriteText(AActiveText, Text);
+ end;
+ end
+ else if not StrIsEmpty(Text, True) then
+ // Unless text is just white space, report an error. We allow white space
+ // since there may be white space between tags. If there is white space we
+ // do nothing - we don't want to write white space.
+ raise EActiveTextParserError.Create(sNoTextPermitted);
+end;
+
+procedure TREMLReader.ParseStartTag(const AActiveText: IActiveText);
+var
+ TagId: TActiveTextActionElemKind; // id of a tag
+resourcestring
+ // Error message
+ sErrMissingParam = 'Expected a "%0:s" parameter value in tag "%1:s"';
+ sErrNesting = 'Illegal nesting of "%0:s" tag';
+ sBadParentBlock = 'Invalid parent block for tag %0:s';
+ sNoTextPermitted = 'Text is not permitted in enclosing block';
+ sMismatchedCloser = 'Mismatching closing block tag %0:s';
+ sErrDocEndExpected = 'End of document expected';
+begin
+ Assert(fLexer.Kind = ttsCompoundStartTag);
+ // Get tag id
+ TagId := TActiveTextActionElemKind(fLexer.TagCode);
+ if TActiveTextElemCaps.DisplayStyleOf(TagId) = dsBlock then
+ begin
+ // Opening block tag found
+ // If writing an implied block, close the block before processing new block
+ if fIsImpliedBlock then
+ EndImpliedBlockElem(AActiveText);
+ // Output block tag if it is valid within parent block, else error
+ if TActiveTextElemCaps.IsPermittedChildElem(
+ fBlockTagStack.Peek.Tag, TagId
+ ) then
+ StartBlockElem(AActiveText, TagId)
+ else
+ raise EActiveTextParserError.CreateFmt(sBadParentBlock, [fLexer.TagName]);
+ end
+ else // TActiveTextElemCaps.DisplayStyleOf(TagId) = dsInline
+ begin
+ // Opeing inline tag found
+ if TActiveTextElemCaps.IsPermittedChildElem(
+ fBlockTagStack.Peek.Tag, TagId
+ ) then
+ // Tag is permitted within parent block: output it
+ StartInlineElem(AActiveText, TagId)
+ else if
+ TActiveTextElemCaps.IsPermittedChildElem(
+ fBlockTagStack.Peek.Tag, ekBlock
+ ) and
+ TActiveTextElemCaps.IsPermittedChildElem(ekBlock, TagId) then
+ begin
+ // Tag not directly permitted, but we can create an implied block iff:
+ // 1. parent block permits an ekBlock child element
+ // 2. ekBlock permits current tag as child element
+ StartImpliedBlockElem(AActiveText);
+ StartInlineElem(AActiveText, TagId);
+ end
+ else
+ // Tag not permitted in parent block: error
+ raise EActiveTextParserError.CreateFmt(sBadParentBlock, [fLexer.TagName]);
+ end;
+end;
+
+procedure TREMLReader.StartBlockElem(const AActiveText: IActiveText;
+ const AElem: TActiveTextActionElemKind);
+begin
+ Assert((TActiveTextElemCaps.DisplayStyleOf(AElem) = dsBlock));
+ StartElem(AActiveText, AElem);
+ fBlockTagStack.Push(TBlockTagInfo.Create(AElem));
+end;
+
+procedure TREMLReader.StartElem(const AActiveText: IActiveText;
+ const AElem: TActiveTextActionElemKind);
+var
+ ParamName: string; // name of a parameter
+ ParamValue: string; // value of a parameter
+ Attr: TActiveTextAttr; // attributes of tag
+resourcestring
+ // Error message
+ sErrMissingParam = 'Expected a "%0:s" parameter value in tag "%1:s"';
+begin
+ // Find any parameters and record element with any parameter
+ TREMLTags.LookupParamName(AElem, ParamName);
+ if ParamName <> '' then
+ begin
+ // We have a parameter: must not be empty
+ ParamValue := fLexer.TagParams.Values[ParamName];
+ if ParamValue = '' then
+ raise EActiveTextParserError.CreateFmt(
+ sErrMissingParam, [ParamName, fLexer.TagName]
+ );
+ // Record param for use by closing tag
+ Attr := TActiveTextAttr.Create(ParamName, ParamValue);
+ fParamStack.Push(Attr);
+ // Add opening action element
+ AActiveText.AddElem(
+ TActiveTextFactory.CreateActionElem(
+ AElem, TActiveTextFactory.CreateAttrs(Attr), fsOpen
+ )
+ );
+ end
+ else
+ begin
+ // No parameter: simply add opening parameterless opening action element
+ AActiveText.AddElem(
+ TActiveTextFactory.CreateActionElem(AElem, fsOpen)
+ );
+ end;
+ // Push tag onto tag stack
+ fTagStack.Push(AElem);
+end;
+
+procedure TREMLReader.StartImpliedBlockElem(const AActiveText: IActiveText);
+begin
+ Assert(not fIsImpliedBlock);
+ StartBlockElem(AActiveText, ekBlock);
+ fIsImpliedBlock := True;
+end;
+
+procedure TREMLReader.StartInlineElem(const AActiveText: IActiveText;
+ const AElem: TActiveTextActionElemKind);
+begin
+ Assert(TActiveTextElemCaps.DisplayStyleOf(AElem) = dsInline);
+ StartElem(AActivetext, AElem);
+ NoteBlockTagHasContent;
+end;
+
function TREMLReader.TagInfo(const TagIdx: Integer; out TagName: string;
out TagCode: Word; out IsContainer: Boolean): Boolean;
{Callback that provides lexer with information about supported tags. Lexer
@@ -525,6 +690,25 @@ function TREMLReader.TagInfo(const TagIdx: Integer; out TagName: string;
end;
end;
+procedure TREMLReader.WriteText(const AActiveText: IActiveText;
+ const AText: string);
+begin
+ // Don't write anything if text is strictly empty string
+ if not StrIsEmpty(AText) then
+ begin
+ AActiveText.AddElem(TActiveTextFactory.CreateTextElem(AText));
+ NoteBlockTagHasContent;
+ end;
+end;
+
+{ TREMLReader.TBlockTagInfo }
+
+constructor TREMLReader.TBlockTagInfo.Create(ATag: TActiveTextActionElemKind);
+begin
+ fTag := ATag;
+ fHasContent := False;
+end;
+
{ TREMLWriter }
constructor TREMLWriter.InternalCreate;
@@ -549,30 +733,33 @@ class function TREMLWriter.Render(const ActiveText: IActiveText): string;
SrcLine: string;
DestLines: IStringList;
DestLine: string;
+ RW: TREMLWriter;
begin
- with InternalCreate do
- try
- Text := '';
- fLevel := 0;
- for Elem in ActiveText do
- begin
- if Supports(Elem, IActiveTextTextElem, TextElem) then
- Text := Text + RenderText(TextElem)
- else if Supports(Elem, IActiveTextActionElem, TagElem) then
- Text := Text + RenderTag(TagElem);
- end;
- SrcLines := TIStringList.Create(Text, EOL, False);
- DestLines := TIStringList.Create;
- for SrcLine in SrcLines do
- begin
- DestLine := StrTrimRight(SrcLine);
- if not StrIsEmpty(DestLine) then
- DestLines.Add(DestLine);
- end;
- Result := DestLines.GetText(EOL, False);
- finally
- Free;
+ if ActiveText.IsEmpty then
+ Exit('');
+ RW := TREMLWriter.InternalCreate;
+ try
+ Text := '';
+ RW.fLevel := 0;
+ for Elem in ActiveText do
+ begin
+ if Supports(Elem, IActiveTextTextElem, TextElem) then
+ Text := Text + RW.RenderText(TextElem)
+ else if Supports(Elem, IActiveTextActionElem, TagElem) then
+ Text := Text + RW.RenderTag(TagElem);
+ end;
+ SrcLines := TIStringList.Create(Text, EOL, False);
+ DestLines := TIStringList.Create;
+ for SrcLine in SrcLines do
+ begin
+ DestLine := StrTrimRight(SrcLine);
+ if not StrIsEmpty(DestLine) then
+ DestLines.Add(DestLine);
end;
+ Result := DestLines.GetText(EOL, False);
+ finally
+ RW.Free;
+ end;
end;
function TREMLWriter.RenderTag(
@@ -582,54 +769,72 @@ function TREMLWriter.RenderTag(
@return Required REML tag.
}
var
- TagName: string; // name of tag
+ TagName: string; // name of tag
ParamName: string; // name of any parameter
begin
- if not TREMLTags.LookupTagName(TagElem.Kind, TagName) then
- raise EBug.CreateFmt('%s.RenderTag: Invalid REML tag id', [ClassName]);
+ TREMLTags.LookupTagName(TagElem.Kind, TagName);
Result := '';
TREMLTags.LookupParamName(TagElem.Kind, ParamName);
case TagElem.State of
fsClose:
begin
// closing tag
- Result := Format('%s>', [TagName]);
- if TActiveTextElemCaps.DisplayStyleOf(TagElem.Kind) = dsBlock then
+ if TagName <> '' then
begin
- Dec(fLevel);
- Result := EOL + StrOfSpaces(IndentMult * fLevel) + Result + EOL;
+ Result := Format('%s>', [TagName]);
+ if TActiveTextElemCaps.DisplayStyleOf(TagElem.Kind) = dsBlock then
+ begin
+ Dec(fLevel);
+ Result := EOL + StrOfSpaces(IndentMult * fLevel) + Result + EOL;
+ fIsStartOfTextLine := True;
+ end;
+ end
+ else
+ begin
+ Result := '';
fIsStartOfTextLine := True;
end;
end;
fsOpen:
begin
// opening tag: may have a parameter
- if ParamName ='' then
- Result := Format('<%s>', [TagName])
- else
- // have a parameter: value must be safely encoded
- Result := Format(
- '<%0:s %1:s="%2:s">',
- [
- TagName,
- ParamName,
- TextToREMLText(TagElem.Attrs[TActiveTextAttrNames.Link_URL])
- ]
- );
- if TActiveTextElemCaps.DisplayStyleOf(TagElem.Kind) = dsBlock then
- begin
- Result := EOL + StrOfSpaces(IndentMult * fLevel) + Result + EOL;
- Inc(fLevel);
- fIsStartOfTextLine := True;
- end
- else if TActiveTextElemCaps.DisplayStyleOf(TagElem.Kind) = dsInline then
+ if TagName <> '' then
begin
- if fIsStartOfTextLine then
+ if ParamName ='' then
+ Result := Format('<%s>', [TagName])
+ else
+ { TODO: revise to not assume parameter must be Link URL }
+ // have a parameter: value must be safely encoded
+ Result := Format(
+ '<%0:s %1:s="%2:s">',
+ [
+ TagName,
+ ParamName,
+ TextToREMLText(TagElem.Attrs[TActiveTextAttrNames.Link_URL])
+ ]
+ );
+ if TActiveTextElemCaps.DisplayStyleOf(TagElem.Kind) = dsBlock then
+ begin
+ Result := EOL + StrOfSpaces(IndentMult * fLevel) + Result + EOL;
+ Inc(fLevel);
+ fIsStartOfTextLine := True;
+ end
+ else if TActiveTextElemCaps.DisplayStyleOf(TagElem.Kind) = dsInline then
begin
- Result := StrOfSpaces(IndentMult * fLevel) + Result;
- fIsStartOfTextLine := False;
+ if fIsStartOfTextLine then
+ begin
+ Result := StrOfSpaces(IndentMult * fLevel) + Result;
+ fIsStartOfTextLine := False;
+ end;
end;
end;
+ end
+ else
+ begin
+ if TagElem.Kind = ekBlock then
+ fIsStartOfTextLine := True;
+ // ekDocument is a no-op and there should be no other elems here
+ Result := '';
end;
end;
end;
@@ -695,6 +900,9 @@ function TREMLWriter.TextToREMLText(const Text: string): string;
fTagMap[8] := TREMLTag.Create(ekUnorderedList, 'ul');
fTagMap[9] := TREMLTag.Create(ekOrderedList, 'ol');
fTagMap[10] := TREMLTag.Create(ekListItem, 'li');
+ // NOTE: ekBlock and ekDocument are not used REML
+ // content of ekBlock is rendered as text outside any block
+ // content of ekDocument is rendered without outputing a tag
end;
class destructor TREMLTags.Destroy;
From ac2f1388d35dd4a5e70195a3a6ce39941ab2631e Mon Sep 17 00:00:00 2001
From: delphidabbler <5164283+delphidabbler@users.noreply.github.com>
Date: Thu, 6 Apr 2023 02:42:26 +0100
Subject: [PATCH 028/189] Major overhaul of active text HTML renderer
Code now based in part on the REML active text writer. The REML based
code was simplified due to the fact that the HTML renderer has tags that
match the ekBlock and ekDocument elements, whereas REML doesn't.
HTML code is now indented to highlight the document structure (although
this doesn't matter really since the code is never displayed to the
user).
---
Src/ActiveText.UHTMLRenderer.pas | 151 ++++++++++++++++---------------
1 file changed, 79 insertions(+), 72 deletions(-)
diff --git a/Src/ActiveText.UHTMLRenderer.pas b/Src/ActiveText.UHTMLRenderer.pas
index 05e3f862f..8a00367b4 100644
--- a/Src/ActiveText.UHTMLRenderer.pas
+++ b/Src/ActiveText.UHTMLRenderer.pas
@@ -47,7 +47,6 @@ TTagInfo = class(TObject)
TCSSStyles = class(TObject)
strict private
var
- fWrapperClass: string;
fElemClassMap: array[TActiveTextActionElemKind] of string;
procedure SetElemClass(ElemKind: TActiveTextActionElemKind;
const Value: string); inline;
@@ -55,7 +54,6 @@ TCSSStyles = class(TObject)
inline;
public
constructor Create;
- property WrapperClass: string read fWrapperClass write fWrapperClass;
property ElemClasses[Kind: TActiveTextActionElemKind]: string
read GetElemClass write SetElemClass;
end;
@@ -63,27 +61,29 @@ TCSSStyles = class(TObject)
var
fCSSStyles: TCSSStyles;
fBuilder: TStringBuilder;
- fInBlock: Boolean;
+ fLevel: Integer;
fTagInfoMap: TTagInfoMap;
+ fIsStartOfTextLine: Boolean;
fLINestingDepth: Cardinal;
+ const
+ IndentMult = 2;
procedure InitialiseTagInfoMap;
- procedure InitialiseRender;
- procedure RenderTextElem(Elem: IActiveTextTextElem);
- procedure RenderBlockActionElem(Elem: IActiveTextActionElem);
- procedure RenderInlineActionElem(Elem: IActiveTextActionElem);
- procedure FinaliseRender;
+ function RenderTag(const TagElem: IActiveTextActionElem): string;
+ function RenderText(const TextElem: IActiveTextTextElem): string;
function MakeOpeningTag(const Elem: IActiveTextActionElem): string;
function MakeClosingTag(const Elem: IActiveTextActionElem): string;
public
constructor Create;
destructor Destroy; override;
function Render(ActiveText: IActiveText): string;
- property Styles: TCSSStyles read fCSSStyles;
end;
implementation
+uses
+ UConsts, UIStringList, UStrUtils;
+
{ TActiveTextHTML }
@@ -107,22 +107,6 @@ destructor TActiveTextHTML.Destroy;
inherited;
end;
-procedure TActiveTextHTML.FinaliseRender;
-begin
- fBuilder.AppendLine(THTML.ClosingTag('div'));
-end;
-
-procedure TActiveTextHTML.InitialiseRender;
-var
- WrapperClassAttr: IHTMLAttributes;
-begin
- if fCSSStyles.WrapperClass <> '' then
- WrapperClassAttr := THTMLAttributes.Create('class', fCSSStyles.WrapperClass)
- else
- WrapperClassAttr := nil;
- fBuilder.AppendLine(THTML.OpeningTag('div', WrapperClassAttr));
-end;
-
procedure TActiveTextHTML.InitialiseTagInfoMap;
var
NullAttrs: TTagInfo.TTagAttrCallback;
@@ -131,7 +115,10 @@ procedure TActiveTextHTML.InitialiseTagInfoMap;
ElemKind: TActiveTextActionElemKind;
const
Tags: array[TActiveTextActionElemKind] of string = (
- 'a', 'strong', 'em', 'var', 'p', 'span', 'h2', 'code', 'ul', 'ol', 'li'
+ 'a' {ekLink}, 'strong' {ekStrong}, 'em' {ekEm}, 'var' {ekVar}, 'p' {ekPara},
+ 'span' {ekWarning}, 'h2' {ekHeading}, 'code' {ekMono},
+ 'ul' {ekUnorderedList}, 'ol' {ekUnorderedList}, 'li' {ekListItem},
+ 'div' {ekBlock}, 'div' {ekDocument}
);
begin
NullAttrs := function(Elem: IActiveTextActionElem): IHTMLAttributes
@@ -178,66 +165,84 @@ function TActiveTextHTML.MakeOpeningTag(const Elem: IActiveTextActionElem):
function TActiveTextHTML.Render(ActiveText: IActiveText): string;
var
- Elem: IActiveTextElem;
- TextElem: IActiveTextTextElem;
- ActionElem: IActiveTextActionElem;
+ Elem: IActiveTextElem; // each element in active text object
+ TextElem: IActiveTextTextElem; // an active text text element
+ TagElem: IActiveTextActionElem; // an active text action element
+ Text: string;
+ SrcLines: IStringList;
+ SrcLine: string;
+ DestLines: IStringList;
+ DestLine: string;
begin
- fBuilder.Clear;
- fInBlock := False;
- InitialiseRender;
+ if ActiveText.IsEmpty then
+ Exit('');
+ Text := '';
+ fLevel := 0;
for Elem in ActiveText do
begin
if Supports(Elem, IActiveTextTextElem, TextElem) then
- RenderTextElem(TextElem)
- else if Supports(Elem, IActiveTextActionElem, ActionElem) then
- begin
- if TActiveTextElemCaps.DisplayStyleOf(ActionElem.Kind) = dsBlock then
- RenderBlockActionElem(ActionElem)
- else
- RenderInlineActionElem(ActionElem);
- end;
+ Text := Text + RenderText(TextElem)
+ else if Supports(Elem, IActiveTextActionElem, TagElem) then
+ Text := Text + RenderTag(TagElem);
end;
- FinaliseRender;
- Result := fBuilder.ToString;
+ SrcLines := TIStringList.Create(Text, EOL, False);
+ DestLines := TIStringList.Create;
+ for SrcLine in SrcLines do
+ begin
+ DestLine := StrTrimRight(SrcLine);
+ if not StrIsEmpty(DestLine) then
+ DestLines.Add(DestLine);
+ end;
+ Result := DestLines.GetText(EOL, False);
end;
-procedure TActiveTextHTML.RenderBlockActionElem(Elem: IActiveTextActionElem);
+function TActiveTextHTML.RenderTag(const TagElem: IActiveTextActionElem):
+ string;
begin
- case Elem.State of
- fsOpen:
- begin
- if Elem.Kind = ekListItem then
- Inc(fLINestingDepth);
- fBuilder.Append(MakeOpeningTag(Elem));
- fInBlock := True;
- end;
+ Result := '';
+ case TagElem.State of
fsClose:
begin
- fInBlock := False;
- fBuilder.AppendLine(MakeClosingTag(Elem));
- if Elem.Kind = ekListItem then
- Dec(fLINestingDepth);
+ Result := MakeClosingTag(TagElem);
+ if TActiveTextElemCaps.DisplayStyleOf(TagElem.Kind) = dsBlock then
+ begin
+ Dec(fLevel);
+ Result := EOL + StrOfSpaces(IndentMult * fLevel) + Result + EOL;
+ fIsStartOfTextLine := True;
+ end;
end;
- end;
-end;
-
-procedure TActiveTextHTML.RenderInlineActionElem(Elem: IActiveTextActionElem);
-begin
- if not fInBlock and (fLINestingDepth = 0) then
- Exit;
- case Elem.State of
fsOpen:
- fBuilder.Append(MakeOpeningTag(Elem));
- fsClose:
- fBuilder.Append(MakeClosingTag(Elem));
+ begin
+ Result := MakeOpeningTag(TagElem);
+ if TActiveTextElemCaps.DisplayStyleOf(TagElem.Kind) = dsBlock then
+ begin
+ Result := EOL + StrOfSpaces(IndentMult * fLevel) + Result + EOL;
+ Inc(fLevel);
+ fIsStartOfTextLine := True;
+ end
+ else if TActiveTextElemCaps.DisplayStyleOf(TagElem.Kind) = dsInline then
+ begin
+ if fIsStartOfTextLine then
+ begin
+ Result := StrOfSpaces(IndentMult * fLevel) + Result;
+ fIsStartOfTextLine := False;
+ end;
+ end;
+ end;
end;
end;
-procedure TActiveTextHTML.RenderTextElem(Elem: IActiveTextTextElem);
+function TActiveTextHTML.RenderText(const TextElem: IActiveTextTextElem):
+ string;
begin
- if not fInBlock and (fLINestingDepth = 0) then
- Exit;
- fBuilder.Append(THTML.Entities(Elem.Text));
+ if fIsStartOfTextLine then
+ begin
+ Result := StrOfSpaces(IndentMult * fLevel);
+ fIsStartOfTextLine := False;
+ end
+ else
+ Result := '';
+ Result := Result + THTML.Entities(TextElem.Text);
end;
{ TActiveTextHTML.TCSSStyles }
@@ -245,13 +250,15 @@ procedure TActiveTextHTML.RenderTextElem(Elem: IActiveTextTextElem);
constructor TActiveTextHTML.TCSSStyles.Create;
const
DefaultClasses: array[TActiveTextActionElemKind] of string = (
- 'external-link', '', '', '', '', 'warning', '', '', '', '', ''
+ 'external-link' {ekLink}, '' {ekStrong}, '' {ekEm}, '' {ekVar}, '' {ekPara},
+ 'warning' {ekWarning}, '' {ekHeading}, '' {ekMono}, '' {ekUnorderedList},
+ '' {ekOrderedList}, '' {ekListItem}, '' {ekBlock},
+ 'active-text' {ekDocument}
);
var
ElemKind: TActiveTextActionElemKind;
begin
inherited Create;
- fWrapperClass := 'active-text';
for ElemKind := Low(TActiveTextActionElemKind)
to High(TActiveTextActionElemKind) do
SetElemClass(ElemKind, DefaultClasses[ElemKind]);
From c34fb83f2f92ce185585debec1eee02c03677175 Mon Sep 17 00:00:00 2001
From: delphidabbler <5164283+delphidabbler@users.noreply.github.com>
Date: Thu, 6 Apr 2023 02:23:54 +0100
Subject: [PATCH 029/189] Overhaul ActiveText.UMain unit
Add new action elements:
ekDocument - wrapper for whole active text document
ekBlock - generic block element
Changed rules of which elements can be permitted as as children of other
elements. The new action elements help to simplify the rules. In turn,
the simplification of rules makes writing parsers and renderers easier.
Revise IActiveText & TActiveText
Add new IActiveText.IsValidActiveTextDocument method with implementation
in TActiveText
Rewrite TActiveText.Append to produce a valid active text document
topped and tailed with ekDocumnent elements
---
Src/ActiveText.UMain.pas | 298 ++++++++++++++++++++++++++++-----------
1 file changed, 213 insertions(+), 85 deletions(-)
diff --git a/Src/ActiveText.UMain.pas b/Src/ActiveText.UMain.pas
index 8d6e54602..648420fee 100644
--- a/Src/ActiveText.UMain.pas
+++ b/Src/ActiveText.UMain.pas
@@ -124,13 +124,15 @@ TActiveTextAttrNames = record
ekStrong, // text formatted as strong (inline)
ekEm, // text formatted as emphasised (inline)
ekVar, // text formatted as variable (inline)
- ekPara, // delimits a paragraph (block level)
+ ekPara, // delimits a paragraph (block)
ekWarning, // text formatted as a warning (inline)
ekHeading, // delimits a heading (block level)
ekMono, // text formatted as mono spaced (inline)
- ekUnorderedList, // container for unordered lists (block level)
- ekOrderedList, // container for ordered list (block level)
- ekListItem // list item (block level)
+ ekUnorderedList, // container for unordered lists (block)
+ ekOrderedList, // container for ordered list (block)
+ ekListItem, // list item (block)
+ ekBlock, // container for unexpected text outside block (block)
+ ekDocument // contains whole document (block)
);
type
@@ -175,6 +177,16 @@ TActiveTextAttrNames = record
/// Appends elements from another given active text object to the
/// current object.
procedure Append(const ActiveText: IActiveText);
+ /// Returns a new IActiveText instance containing just the first
+ /// block of the current object.
+ ///
+ /// The first block is the content of the block level tag that starts
+ /// the active text. If this block has child blocks (for e.g. an unordered
+ /// list) then they are included.
+ /// If the current object is empty then an empty object is returned.
+ ///
+ ///
+ function FirstBlock: IActiveText;
/// Checks if the active text object contains any elements.
///
function IsEmpty: Boolean;
@@ -184,6 +196,11 @@ TActiveTextAttrNames = record
/// elements except for "para". This can rendered in plain text with no
/// loss of formatting.
function IsPlainText: Boolean;
+ /// Checks if the active text object is a valid active text
+ /// document.
+ /// A valid document is either empty or it is surrounded by
+ /// matching ekDocument elements.
+ function IsValidActiveTextDocument: Boolean;
/// Returns element at given index in active text object's element
/// list.
function GetElem(Idx: Integer): IActiveTextElem;
@@ -283,22 +300,28 @@ TCaps = record
var
/// Determines how element is to be displayed.
DisplayStyle: TActiveTextDisplayStyle;
- /// Set of elements that may not occur inside the element.
- ///
- Exclusions: TActiveTextActionElemKinds;
- /// Set of elements that are permitted as parents of the
- /// element.
- /// An empty set is taken to mean any element is permitted.
- ///
- RequiredParents: TActiveTextActionElemKinds;
/// Specifies whether plain text can be contained within the
/// element.
PermitsText: Boolean;
+ /// Specifies the elements that are permitted as child
+ /// elements of this element.
+ PermittedChildElems: TActiveTextActionElemKinds;
end;
const
/// Set of block level elements.
BlockElems = [
- ekPara, ekHeading, ekUnorderedList, ekOrderedList, ekListItem
+ ekPara, ekHeading, ekUnorderedList, ekOrderedList, ekListItem,
+ ekBlock, ekDocument
+ ];
+ /// Set of block level elements that can directly contain text
+ /// and inline elements.
+ TextContentBlocks = [
+ ekPara, ekHeading, ekBlock
+ ];
+ /// Set of block level elements that can contain only blocks
+ /// that are not container blocks.
+ ContainerBlocks = [
+ ekDocument, ekListItem
];
/// Set of inline elements.
InlineElems = [
@@ -313,90 +336,94 @@ TCaps = record
// ekLink
// may contain any inline elements but no block elements
DisplayStyle: dsInline;
- Exclusions: BlockElems;
- RequiredParents: [];
PermitsText: True;
+ PermittedChildElems: InlineElems - [ekLink];
),
(
// ekStrong
// may contain any inline elements but no block elements
DisplayStyle: dsInline;
- Exclusions: BlockElems;
- RequiredParents: [];
PermitsText: True;
+ PermittedChildElems: InlineElems;
),
(
// ekEm
// may contain any inline elements but no block elements
DisplayStyle: dsInline;
- Exclusions: BlockElems;
- RequiredParents: [];
PermitsText: True;
+ PermittedChildElems: InlineElems;
),
(
// ekVar
// may contain any inline elements but no block elements
DisplayStyle: dsInline;
- Exclusions: BlockElems;
- RequiredParents: [];
PermitsText: True;
+ PermittedChildElems: InlineElems;
),
(
// ekPara
// may contain any inline elements but no block elements
DisplayStyle: dsBlock;
- Exclusions: BlockElems;
- RequiredParents: [];
PermitsText: True;
+ PermittedChildElems: InlineElems;
),
(
// ekWarning
// may contain any inline elements but no block elements
DisplayStyle: dsInline;
- Exclusions: BlockElems;
- RequiredParents: [];
PermitsText: True;
+ PermittedChildElems: InlineElems;
),
(
// ekHeading
// may contain any inline elements but no block elements
DisplayStyle: dsBlock;
- Exclusions: BlockElems;
- RequiredParents: [];
PermitsText: True;
+ PermittedChildElems: InlineElems;
),
(
// ekMono
// may contain any inline elements but no block elements
DisplayStyle: dsInline;
- Exclusions: BlockElems;
- RequiredParents: [];
PermitsText: True;
+ PermittedChildElems: InlineElems;
),
(
// ekUnorderedList
// may contain only list item elements
DisplayStyle: dsBlock;
- Exclusions: AllElems - [ekListItem];
- RequiredParents: [];
- PermitsText: False
+ PermitsText: False;
+ PermittedChildElems: [ekListItem];
),
(
// ekOrderedList
// may contain only list item elements
DisplayStyle: dsBlock;
- Exclusions: AllElems - [ekListItem];
- RequiredParents: [];
PermitsText: False;
+ PermittedChildElems: [ekListItem];
),
(
// ekListItem
- // may contain any inline or block elements except another list
- // item
+ // may contain only block elements, but not itself or other
+ // block containers
+ DisplayStyle: dsBlock;
+ PermitsText: False;
+ PermittedChildElems: BlockElems - ContainerBlocks;
+ ),
+ (
+ // ekBlock
+ // may contain any inline elements but no block elements
DisplayStyle: dsBlock;
- Exclusions: [ekListItem];
- RequiredParents: [ekOrderedList, ekUnorderedList];
PermitsText: True;
+ PermittedChildElems: InlineElems;
+ ),
+ (
+ // ekDocument
+ // may contain only block elements, but not itself or other
+ // block containers
+ DisplayStyle: dsBlock;
+ PermitsText: False;
+ PermittedChildElems: BlockElems - ContainerBlocks;
)
);
public
@@ -406,24 +433,10 @@ TCaps = record
/// Checks whether the given element can contain text.
class function CanContainText(const Elem: TActiveTextActionElemKind):
Boolean; static;
- /// Checks whether the given Parent element can contain the given
- /// Child element.
- class function CanContainElem(
- const Parent, Child: TActiveTextActionElemKind): Boolean; static;
- /// Checks whether the given Parent element is required as a
- /// parent of the given Child element.
- class function IsRequiredParent(
- const Parent, Child: TActiveTextActionElemKind): Boolean; static;
- /// Checks whether the given element is permitted in the root of
- /// an active text document, i.e. outside any other block level element.
- ///
- class function IsElemPermittedInRoot(const Elem: TActiveTextActionElemKind):
- Boolean; static;
- /// Checks whether the given child element is excluded from being
- /// a child of the given parent element.
- class function IsExcludedElem(
+ /// Checks whether the given child element is permitted as a child
+ /// of the given parent element.
+ class function IsPermittedChildElem(
const Parent, Child: TActiveTextActionElemKind): Boolean; static;
-
end;
@@ -434,7 +447,10 @@ implementation
// Delphi
SysUtils,
// Project
- IntfCommon;
+ IntfCommon,
+ UConsts,
+ UStrUtils,
+ UUtils;
type
@@ -474,6 +490,17 @@ TActiveText = class(TInterfacedObject,
///
/// Method of IActiveText.
procedure Append(const ActiveText: IActiveText);
+ /// Returns a new IActiveText instance containing just the first
+ /// block of the current object.
+ ///
+ /// The first block is the content of the block level tag that starts
+ /// the active text. If this block has child blocks (for e.g. an unordered
+ /// list) then they are included.
+ /// If the current object is empty then an empty object is returned.
+ ///
+ /// Method of IActiveText.
+ ///
+ function FirstBlock: IActiveText;
/// Checks if the element list is empty.
/// Method of IActiveText.
function IsEmpty: Boolean;
@@ -486,6 +513,14 @@ TActiveText = class(TInterfacedObject,
/// Method of IActiveText.
///
function IsPlainText: Boolean;
+ /// Checks if the active text object is a valid active text
+ /// document.
+ ///
+ /// A valid document is either empty or it is surrounded by matching
+ /// ekDocument elements.
+ /// Method of IActiveText.
+ ///
+ function IsValidActiveTextDocument: Boolean;
/// Returns element at given index in element list.
/// Method of IActiveText.
function GetElem(Idx: Integer): IActiveTextElem;
@@ -681,15 +716,43 @@ function TActiveText.AddElem(const Elem: IActiveTextElem): Integer;
end;
procedure TActiveText.Append(const ActiveText: IActiveText);
+
+ function IsDocumentElem(Elem: IActiveTextElem): Boolean;
+ var
+ ActiveElem: IActiveTextActionElem;
+ begin
+ if not Supports(Elem, IActiveTextActionElem, ActiveElem) then
+ Exit(False);
+ Result := ActiveElem.Kind = ekDocument;
+ end;
+
var
Elem: IActiveTextElem; // references each element in elems
- NewElem: IActiveTextElem;
+ SelfCopy: IActiveText; // temporary copy of this object
begin
+ // *** Don't call Clone or Assign here: they call backinto this method.
+
+ // Make a copy of elements of self
+ SelfCopy := TActiveText.Create;
+ for Elem in fElems do
+ SelfCopy.AddElem((Elem as IClonable).Clone as IActiveTextElem);
+
+ // Clear own elems and add document start element
+ fElems.Clear;
+ AddElem(TActiveTextFactory.CreateActionElem(ekDocument, fsOpen));
+
+ // Copy own elements back to fElems, skipping ekDocument elems
+ for Elem in SelfCopy do
+ if not IsDocumentElem(Elem) then
+ AddElem((Elem as IClonable).Clone as IActiveTextElem);
+
+ // Copy active text to be assigned, skipping its ekDocument elems
for Elem in ActiveText do
- begin
- NewElem := (Elem as IClonable).Clone as IActiveTextElem;
- AddElem(NewElem);
- end;
+ if not IsDocumentElem(Elem) then
+ AddElem((Elem as IClonable).Clone as IActiveTextElem);
+
+ // Add closing ekDocument Elem
+ AddElem(TActiveTextFactory.CreateActionElem(ekDocument, fsClose));
end;
procedure TActiveText.Assign(const Src: IInterface);
@@ -719,6 +782,78 @@ destructor TActiveText.Destroy;
inherited;
end;
+function TActiveText.FirstBlock: IActiveText;
+var
+ Elem: IActiveTextElem;
+ ActionElem: IActiveTextActionElem;
+ Block: IActiveTextActionElem;
+ Idx: Integer;
+ EndOfBlockFound: Boolean;
+ HasDocElems: Boolean;
+ FirstBlockIdx: Integer;
+begin
+ Result := TActiveText.Create;
+ if IsEmpty then
+ Exit;
+
+ HasDocElems := IsValidActiveTextDocument;
+ if HasDocElems then
+ begin
+ // We have ekDocument elements wrapping document: 1st true blue should be
+ // next element
+ if GetCount < 4 then
+ Exit;
+ FirstBlockIdx := 1;
+ end
+ else
+ begin
+ // No ekDocument elements: 1st true block is should be first element
+ if GetCount < 2 then
+ Exit;
+ FirstBlockIdx := 0;
+ end;
+
+ // Element at FirstBlockIdx must be a valid block opening element
+ Elem := GetElem(FirstBlockIdx);
+ GetIntf(Elem, IActiveTextElem, Block);
+ if not Assigned(Block)
+ or (TActiveTextElemCaps.DisplayStyleOf(Block.Kind) <> dsBlock)
+ or (Block.State <> fsOpen) then
+ raise EBug.Create(
+ ClassName + '.FirstBlock: block opener expected after ekDocument element'
+ );
+
+ // We have required block: add document opener element and block element
+ Result.AddElem(TActiveTextFactory.CreateActionElem(ekDocument, fsOpen));
+ Result.AddElem(Elem);
+
+ // Scan through remaining elements, copying them to output as we go. Halt when
+ // (or if) matching closing block found.
+ EndOfBlockFound := False;
+ Idx := Succ(FirstBlockIdx);
+ while Idx < Pred(GetCount) do
+ begin
+ Elem := GetElem(Idx);
+ Result.AddElem(Elem);
+ if Supports(Elem, IActiveTextActionElem, ActionElem)
+ and (ActionElem.Kind = Block.Kind)
+ and (ActionElem.State = fsClose) then
+ begin
+ EndOfBlockFound := True;
+ Break;
+ end;
+ Inc(Idx);
+ end;
+ // No closing block found
+ if not EndOfBlockFound then
+ raise EBug.Create(
+ ClassName + '.FirstBlock: Matching closer for first block not found'
+ );
+
+ // Add document close elem (closing block elem added in loop above)
+ Result.AddElem(TActiveTextFactory.CreateActionElem(ekDocument, fsClose));
+end;
+
function TActiveText.GetCount: Integer;
begin
Result := fElems.Count;
@@ -747,12 +882,25 @@ function TActiveText.IsPlainText: Boolean;
for Elem in fElems do
begin
if Supports(Elem, IActiveTextActionElem, ActionElem)
- and (ActionElem.Kind <> ekPara) then
+ and not (ActionElem.Kind in [ekPara, ekDocument]) then
Exit(False);
end;
Result := True;
end;
+function TActiveText.IsValidActiveTextDocument: Boolean;
+var
+ DocStartElem, DocEndElem: IActiveTextActionElem;
+begin
+ if IsEmpty then
+ Exit(True);
+ Result := (GetCount >= 2)
+ and Supports(fElems[0], IActiveTextActionElem, DocStartElem)
+ and (DocStartElem.Kind = ekDocument) and (DocStartElem.State = fsOpen)
+ and Supports(fElems[Pred(GetCount)], IActiveTextActionElem, DocEndElem)
+ and (DocEndElem.Kind = ekDocument) and (DocEndElem.State = fsClose);
+end;
+
function TActiveText.ToString: string;
var
Elem: IActiveTextElem;
@@ -773,7 +921,7 @@ function TActiveText.ToString: string;
// from text at start of following block
SB.AppendLine;
end;
- Result := SB.ToString;
+ Result := StrTrimRight(SB.ToString) + EOL; // ensure single final EOL(s)
finally
SB.Free;
end;
@@ -899,12 +1047,6 @@ function TActiveTextAttrs.GetEnumerator: TEnumerator>;
{ TActiveTextElemCapsMap }
-class function TActiveTextElemCaps.CanContainElem(const Parent,
- Child: TActiveTextActionElemKind): Boolean;
-begin
- Result := not (Child in Map[Parent].Exclusions);
-end;
-
class function TActiveTextElemCaps.CanContainText(
const Elem: TActiveTextActionElemKind): Boolean;
begin
@@ -917,24 +1059,10 @@ class function TActiveTextElemCaps.DisplayStyleOf(
Result := Map[Elem].DisplayStyle;
end;
-class function TActiveTextElemCaps.IsElemPermittedInRoot(
- const Elem: TActiveTextActionElemKind): Boolean;
-begin
- Result := Map[Elem].RequiredParents = [];
-end;
-
-class function TActiveTextElemCaps.IsExcludedElem(const Parent,
- Child: TActiveTextActionElemKind): Boolean;
-begin
- Result := Child in Map[Parent].Exclusions;
-end;
-
-class function TActiveTextElemCaps.IsRequiredParent(
+class function TActiveTextElemCaps.IsPermittedChildElem(
const Parent, Child: TActiveTextActionElemKind): Boolean;
begin
- if Map[Child].RequiredParents = [] then
- Exit(True);
- Result := Parent in Map[Child].RequiredParents;
+ Result := Child in Map[Parent].PermittedChildElems;
end;
end.
From 66690fa86fa918d3a074a40576ff660daa12837a Mon Sep 17 00:00:00 2001
From: delphidabbler <5164283+delphidabbler@users.noreply.github.com>
Date: Thu, 6 Apr 2023 02:29:39 +0100
Subject: [PATCH 030/189] Update TActiveTextValidator and private TErrorInfo
Validator was updated to check that any non-empty active text is topped
and tailed by ekDocument elements.
Rewrite TActiveTextValidator.ValidateDocumentStructure to call into
IActiveText.IsValidActiveTextDocument to do the heavy lifting.
Removed unused Element field from TErrorInfo private advanced record of
TActiveTextValidator. Modified constructor and calls to it accordingly.
---
Src/ActiveText.UValidator.pas | 35 +++++++++++++++++++++++++----------
1 file changed, 25 insertions(+), 10 deletions(-)
diff --git a/Src/ActiveText.UValidator.pas b/Src/ActiveText.UValidator.pas
index 898fcc265..96aa2e77a 100644
--- a/Src/ActiveText.UValidator.pas
+++ b/Src/ActiveText.UValidator.pas
@@ -36,16 +36,11 @@ TErrorInfo = record
public
/// Error code.
Code: TErrorCode;
- /// Reference to element causing problem.
- /// May be nil if error doesn't relate to an element.
- ///
- Element: IActiveTextElem;
/// Description of error.
Description: string;
/// Constructs a record. Sets fields from parameter values.
///
- constructor Create(const ACode: TErrorCode; AElement: IActiveTextElem;
- const ADescription: string); overload;
+ constructor Create(const ACode: TErrorCode; const ADescription: string);
end;
strict private
/// Validates given link element.
@@ -56,6 +51,14 @@ TErrorInfo = record
/// Boolean. True on success or False on failure.
class function ValidateLink(LinkElem: IActiveTextActionElem;
out ErrInfo: TErrorInfo): Boolean; static;
+ /// Validates document structure.
+ /// IActiveText [in] Active text to be validated.
+ ///
+ /// TErrorInfo [out] Contains error information if
+ /// validation fails. Undefined if validation succeeds.
+ /// Boolean. True on success or False on failure.
+ class function ValidateDocumentStructure(ActiveText: IActiveText;
+ out ErrInfo: TErrorInfo): Boolean; static;
public
/// Validates given active text.
/// IActiveText [in] Active text to be validated.
@@ -92,6 +95,9 @@ class function TActiveTextValidator.Validate(ActiveText: IActiveText;
begin
if ActiveText.IsEmpty then
Exit(True);
+ // Validate document structure
+ if not ValidateDocumentStructure(ActiveText, ErrInfo) then
+ Exit(False);
// Validate elements
for Elem in ActiveText do
begin
@@ -115,6 +121,16 @@ class function TActiveTextValidator.Validate(ActiveText: IActiveText): Boolean;
Result := Validate(ActiveText, Dummy);
end;
+class function TActiveTextValidator.ValidateDocumentStructure(
+ ActiveText: IActiveText; out ErrInfo: TErrorInfo): Boolean;
+resourcestring
+ sNoDocTags = 'Document must start and end with document tags';
+begin
+ Result := ActiveText.IsValidActiveTextDocument;
+ if not Result then
+ ErrInfo := TErrorInfo.Create(errBadStructure, sNoDocTags);
+end;
+
class function TActiveTextValidator.ValidateLink(
LinkElem: IActiveTextActionElem; out ErrInfo: TErrorInfo): Boolean;
resourcestring
@@ -150,7 +166,7 @@ TProtocolInfo = record
< Length(PI.Protocol) + PI.MinURLLength then
begin
ErrInfo := TErrorInfo.Create(
- errBadLinkURL, LinkElem, Format(sURLLengthErr, [URL])
+ errBadLinkURL, Format(sURLLengthErr, [URL])
);
Exit(False);
end;
@@ -160,17 +176,16 @@ TProtocolInfo = record
// No supported protocol
Result := False;
ErrInfo := TErrorInfo.Create(
- errBadLinkProtocol, LinkElem, Format(sURLProtocolErr, [URL])
+ errBadLinkProtocol, Format(sURLProtocolErr, [URL])
);
end;
{ TActiveTextValidator.TErrorInfo }
constructor TActiveTextValidator.TErrorInfo.Create(const ACode: TErrorCode;
- AElement: IActiveTextElem; const ADescription: string);
+ const ADescription: string);
begin
Code := ACode;
- Element := AElement;
Description := ADescription;
end;
From 9b947edb67f045ed898bbde4e29a15bed5c6939c Mon Sep 17 00:00:00 2001
From: delphidabbler <5164283+delphidabbler@users.noreply.github.com>
Date: Thu, 6 Apr 2023 17:28:48 +0100
Subject: [PATCH 031/189] Heavily revise USnippetExtraHelper unit
1. Moved class that parses old style credits text into active text from
USnippetCreditsParser into the TSnippetExtraHelper.TCreditsParser
private class in USnippetExtraHelper.
2. Removed the now redundant USnippetCreditsParser unit from project.
3. Modify methods of TSnippetExtraHelper that create active text
documents to embed the active text between matched ekDocument elements.
4. Rewrote TSnippetExtraHelper.BuildActiveText to simply hand off the
work by calling to the TActiveTextFactory.CreateActiveText parser
overload, passing the REML parser.
---
Src/CodeSnip.dpr | 1 -
Src/CodeSnip.dproj | 1 -
Src/USnippetCreditsParser.pas | 166 ------------------------
Src/USnippetExtraHelper.pas | 235 +++++++++++++++++++++-------------
4 files changed, 143 insertions(+), 260 deletions(-)
delete mode 100644 Src/USnippetCreditsParser.pas
diff --git a/Src/CodeSnip.dpr b/Src/CodeSnip.dpr
index e12778a39..60436f1a5 100644
--- a/Src/CodeSnip.dpr
+++ b/Src/CodeSnip.dpr
@@ -317,7 +317,6 @@ uses
USingleton in 'USingleton.pas',
USnipKindListAdapter in 'USnipKindListAdapter.pas',
USnippetAction in 'USnippetAction.pas',
- USnippetCreditsParser in 'USnippetCreditsParser.pas',
USnippetDoc in 'USnippetDoc.pas',
USnippetExtraHelper in 'USnippetExtraHelper.pas',
USnippetHTML in 'USnippetHTML.pas',
diff --git a/Src/CodeSnip.dproj b/Src/CodeSnip.dproj
index 14d04cbd9..af19fb220 100644
--- a/Src/CodeSnip.dproj
+++ b/Src/CodeSnip.dproj
@@ -519,7 +519,6 @@
-
diff --git a/Src/USnippetCreditsParser.pas b/Src/USnippetCreditsParser.pas
deleted file mode 100644
index 07e784073..000000000
--- a/Src/USnippetCreditsParser.pas
+++ /dev/null
@@ -1,166 +0,0 @@
-{
- * This Source Code Form is subject to the terms of the Mozilla Public License,
- * v. 2.0. If a copy of the MPL was not distributed with this file, You can
- * obtain one at https://mozilla.org/MPL/2.0/
- *
- * Copyright (C) 2008-2021, Peter Johnson (gravatar.com/delphidabbler).
- *
- * Provides an implementation of IActiveTextParser that can parse the markup
- * used in Credits elements of data files and convert the markup into an active
- * text object.
-}
-
-
-unit USnippetCreditsParser;
-
-
-interface
-
-
-uses
- // Project
- ActiveText.UMain;
-
-
-type
-
- /// Class that parses markup used in Credits element read from
- /// snippets data files. Markup is translated into active text.
- /// The Credits element may occur in main database files and v1 of
- /// the user database and export files.
- TSnippetCreditsParser = class(TInterfacedObject, IActiveTextParser)
- strict private
- var
- /// URL to be used in any link contained in markup.
- fURL: string;
- public
- /// Object constructor. Sets up object.
- /// string [in] URL to be used in any hyperlinks defined
- /// by Credit markup.
- constructor Create(const URL: string);
- /// Parses markup and updates active text object.
- /// string [in] Markup containing definition of active
- /// text. Must be valid Credits element markup.
- /// IActiveText [in] Active text object updated by
- /// parser.
- /// Implements IActiveTextParser.Parse.
- procedure Parse(const Markup: string; const ActiveText: IActiveText);
- end;
-
-
-implementation
-
-
-{
- About the "Credits" Markup
- --------------------------
- The markup is simple. It is just plain text with at most one group of text
- delimited by '[' and ']' characters. The text enclosed in brackets represents
- a hyperlink. The destination URL of the hyperlink is given by the URL
- parameter passed to the constructor.
-
- Examples:
- "Some markup without a link."
- "Some markup with a [link]."
-}
-
-
-uses
- // Project
- UStrUtils;
-
-
-{ TSnippetCreditsParser }
-
-constructor TSnippetCreditsParser.Create(const URL: string);
-begin
- inherited Create;
- fURL := URL;
-end;
-
-procedure TSnippetCreditsParser.Parse(const Markup: string;
- const ActiveText: IActiveText);
-const
- cOpenBracket = '['; // open bracket character that starts a link
- cCloseBracket = ']'; // close bracket character that ends a link
-resourcestring
- // Error messages
- sUnexpectedCloser = 'Unexpected closing bracket found';
- sUnterminatedLink = 'Unterminated link';
- sEmptyLink = 'Empty link definition';
- sWrongBracketOrder = 'Close bracket preceeds link open bracket';
- sMultipleOpeners = 'More than one open bracket is present';
- sMultipleClosers = 'More than one close bracket is present';
- sNoURL = 'No URL specified';
-var
- OpenBracketPos: Integer; // position of opening bracket in markup
- CloseBracketPos: Integer; // position of closing bracket in markup
- Prefix, Postfix: string; // text before and after link (can be empty)
- LinkText: string; // link text
-begin
- // Find open and closing brackets that delimit link text
- OpenBracketPos := StrPos(cOpenBracket, Markup);
- CloseBracketPos := StrPos(cCloseBracket, Markup);
- if OpenBracketPos = 0 then
- begin
- // No links: plain text only
- // check for errors
- if CloseBracketPos > 0 then
- raise EActiveTextParserError.Create(sUnexpectedCloser);
- // record text element
- ActiveText.AddElem(TActiveTextFactory.CreateTextElem(Markup));
- end
- else
- begin
- // We have a potential link
- // check for errors
- if CloseBracketPos = 0 then
- raise EActiveTextParserError.Create(sUnterminatedLink);
- if CloseBracketPos = OpenBracketPos + 1 then
- raise EActiveTextParserError.Create(sEmptyLink);
- if CloseBracketPos < OpenBracketPos then
- raise EActiveTextParserError.Create(sWrongBracketOrder);
- if StrCountDelims(cOpenBracket, Markup) > 1 then
- raise EActiveTextParserError.Create(sMultipleOpeners);
- if StrCountDelims(cCloseBracket, Markup) > 1 then
- raise EActiveTextParserError.Create(sMultipleClosers);
- // must have a URL
- if fURL = '' then
- raise EActiveTextParserError.Create(sNoURL);
- // get the various components
- LinkText := StrSlice(
- Markup, OpenBracketPos + 1, CloseBracketPos - OpenBracketPos - 1
- );
- Assert(LinkText <> '',
- ClassName + '.Parse: Link text is '' but has passed check');
- Prefix := StrSliceLeft(Markup, OpenBracketPos - 1);
- Postfix := StrSliceRight(Markup, Length(Markup) - CloseBracketPos);
- // record the elements
- if Prefix <> '' then
- ActiveText.AddElem(TActiveTextFactory.CreateTextElem(Prefix));
- ActiveText.AddElem(
- TActiveTextFactory.CreateActionElem(
- ekLink,
- TActiveTextFactory.CreateAttrs(
- TActiveTextAttr.Create(TActiveTextAttrNames.Link_URL, fURL)
- ),
- fsOpen
- )
- );
- ActiveText.AddElem(TActiveTextFactory.CreateTextElem(LinkText));
- ActiveText.AddElem(
- TActiveTextFactory.CreateActionElem(
- ekLink,
- TActiveTextFactory.CreateAttrs(
- TActiveTextAttr.Create(TActiveTextAttrNames.Link_URL, fURL)
- ),
- fsClose
- )
- );
- if Postfix <> '' then
- ActiveText.AddElem(TActiveTextFactory.CreateTextElem(Postfix));
- end;
-end;
-
-end.
-
diff --git a/Src/USnippetExtraHelper.pas b/Src/USnippetExtraHelper.pas
index fafad6eea..f813d543a 100644
--- a/Src/USnippetExtraHelper.pas
+++ b/Src/USnippetExtraHelper.pas
@@ -29,11 +29,52 @@ interface
text and vice versa.
}
TSnippetExtraHelper = class(TNoConstructObject)
+ strict private
+ type
+ /// Class that parses markup used in Credits element read from
+ /// snippets data files. Markup is translated into active text.
+ ///
+ /// Generated active text IS NOT embedded in an ekDocument block.
+ ///
+ /// The Credits element may occur in main database files and v1 of
+ /// the user database and export files.
+ /// Credits markup is simple. It is just plain text with at most
+ /// one group of text delimited by '[' and ']' characters. The text
+ /// enclosed in brackets represents a hyperlink. The destination URL of
+ /// the hyperlink is given by the URL parameter passed to the
+ /// constructor.
+ /// Eamples:
+ /// Some markup without a link.
+ /// Some markup with a [link].
+ ///
+ TCreditsParser = class(TInterfacedObject, IActiveTextParser)
+ strict private
+ var
+ /// URL to be used in any link contained in markup.
+ ///
+ fURL: string;
+ public
+ /// Object constructor. Sets up object.
+ /// string [in] URL to be used in any hyperlinks
+ /// defined by Credit markup.
+ constructor Create(const URL: string);
+ /// Parses markup and updates active text object.
+ /// string [in] Markup containing definition of
+ /// active text. Must be valid Credits element markup.
+ /// IActiveText [in] Active text object
+ /// updated by parser.
+ ///
+ /// NOTE: Does not wrap generated text in any block tags,
+ /// including top level document tags.
+ /// Implements IActiveTextParser.Parse.
+ ///
+ procedure Parse(const Markup: string; const ActiveText: IActiveText);
+ end;
public
class function BuildActiveText(const PrefixText, CreditsMarkup,
URL: string): IActiveText; overload;
- {Builds an active text object containing some plain followed by active
- text defined by markup in the "Credits" format.
+ {Builds an active text object containing some plain text followed by
+ active text defined by markup in the "Credits" format.
@param PrefixText [in] PrefixText text. If not empty string this is
added as plain text before any credits markup.
@param CreditsMarkup [in] "Credits" markup. May contain a link indicated
@@ -71,7 +112,7 @@ implementation
// Delphi
SysUtils,
// Project
- UREMLDataIO, USnippetCreditsParser, UStrUtils;
+ UREMLDataIO, UStrUtils;
{ TSnippetExtraHelper }
@@ -92,6 +133,7 @@ class function TSnippetExtraHelper.BuildActiveText(const PrefixText,
begin
// Create new empty active text object
Result := TActiveTextFactory.CreateActiveText;
+ Result.AddElem(TActiveTextFactory.CreateActionElem(ekDocument, fsOpen));
if (PrefixText <> '') then
begin
// We have prefix text: add it to result as a paragraph containing a single
@@ -109,11 +151,12 @@ class function TSnippetExtraHelper.BuildActiveText(const PrefixText,
Result.Append(
TActiveTextFactory.CreateActiveText(
StrMakeSentence(CreditsMarkup),
- TSnippetCreditsParser.Create(URL)
+ TCreditsParser.Create(URL)
)
);
Result.AddElem(TActiveTextFactory.CreateActionElem(ekPara, fsClose));
end;
+ Result.AddElem(TActiveTextFactory.CreateActionElem(ekDocument, fsClose));
end;
class function TSnippetExtraHelper.BuildActiveText(
@@ -123,96 +166,10 @@ class function TSnippetExtraHelper.BuildActiveText(
@return Required active text object. Will be an empty object if REML is
empty string.
}
-
- // Check for an opening block tag
- function IsBlockOpener(Elem: IActiveTextElem): Boolean;
- var
- ActionElem: IActiveTextActionElem;
- begin
- if not Supports(Elem, IActiveTextActionElem, ActionElem) then
- Exit(False);
- Result := (TActiveTextElemCaps.DisplayStyleOf(ActionElem.Kind) = dsBlock)
- and (ActionElem.State = fsOpen);
- end;
-
- // Check for a closing block tag
- function IsBlockCloser(Elem: IActiveTextElem): Boolean;
- var
- ActionElem: IActiveTextActionElem;
- begin
- if not Supports(Elem, IActiveTextActionElem, ActionElem) then
- Exit(False);
- Result := (TActiveTextElemCaps.DisplayStyleOf(ActionElem.Kind) = dsBlock)
- and (ActionElem.State = fsClose);
- end;
-
- // Embed given content in a para block and append to result, unless content is
- // empty when do nothing.
- procedure AddNoneEmptyParaToResult(ParaContent: IActiveText);
- begin
- if ParaContent.IsEmpty then
- Exit;
- if StrTrim(ParaContent.ToString) = '' then
- Exit;
- Result.AddElem(TActiveTextFactory.CreateActionElem(ekPara, fsOpen));
- Result.Append(ParaContent);
- Result.AddElem(TActiveTextFactory.CreateActionElem(ekPara, fsClose));
- end;
-
-var
- ActiveText: IActiveText; // receives active text built from REML
- OutsideBlockActiveText: IActiveText; // receives text outside of blocks
- Elem: IActiveTextElem; // each element in active text
- Level: Integer; // depth of block levels
begin
- Result := TActiveTextFactory.CreateActiveText;
- if REML = '' then
- Exit;
// Create active text by parsing REML
- ActiveText := TActiveTextFactory.CreateActiveText(REML, TREMLReader.Create);
- if ActiveText.IsEmpty then
- Exit;
- // Init block level & obj used to accumulate text outside blocks
- Level := 0;
- OutsideBlockActiveText := TActiveTextFactory.CreateActiveText;
- for Elem in ActiveText do
- begin
- if IsBlockOpener(Elem) then
- begin
- // We have block opener tag. Check for any text that preceeded a level
- // zero block and wrap it in a paragraph before writing the block opener
- if Level = 0 then
- begin
- if not OutsideBlockActiveText.IsEmpty then
- begin
- AddNoneEmptyParaToResult(OutsideBlockActiveText);
- OutsideBlockActiveText := TActiveTextFactory.CreateActiveText;
- end;
- end;
- Result.AddElem(Elem);
- Inc(Level); // drop down one level
- end
- else if IsBlockCloser(Elem) then
- begin
- // Block closer
- Dec(Level);
- Result.AddElem(Elem); // climb up one level
- end
- else
- begin
- // Not block opener or closer
- // If we're outside any block, append elem to store of elems not included
- // in blocks. If we're in a block, just add the elem to output
- if Level = 0 then
- OutsideBlockActiveText.AddElem(Elem)
- else
- Result.AddElem(Elem);
- end;
- end;
- Assert(Level = 0, ClassName + '.BuildActiveText: Unbalanced blocks');
- // Write any outstanding elems that occured outside a block
- if not OutsideBlockActiveText.IsEmpty then
- AddNoneEmptyParaToResult(OutsideBlockActiveText);
+ // .. the REML parser returns correct document or empty object if REML=''
+ Result := TActiveTextFactory.CreateActiveText(REML, TREMLReader.Create);
end;
class function TSnippetExtraHelper.BuildREMLMarkup(
@@ -232,11 +189,105 @@ class function TSnippetExtraHelper.PlainTextToActiveText(
Text := StrTrim(Text);
if Text = '' then
Exit;
+ Result.AddElem(TActiveTextFactory.CreateActionElem(ekDocument, fsOpen));
Result.AddElem(TActiveTextFactory.CreateActionElem(ekPara, fsOpen));
Result.AddElem(
TActiveTextFactory.CreateTextElem(Text)
);
Result.AddElem(TActiveTextFactory.CreateActionElem(ekPara, fsClose));
+ Result.AddElem(TActiveTextFactory.CreateActionElem(ekDocument, fsClose));
+end;
+
+{ TSnippetExtraHelper.TCreditsParser }
+
+constructor TSnippetExtraHelper.TCreditsParser.Create(const URL: string);
+begin
+ inherited Create;
+ fURL := URL;
+end;
+
+procedure TSnippetExtraHelper.TCreditsParser.Parse(const Markup: string;
+ const ActiveText: IActiveText);
+const
+ cOpenBracket = '['; // open bracket character that starts a link
+ cCloseBracket = ']'; // close bracket character that ends a link
+resourcestring
+ // Error messages
+ sUnexpectedCloser = 'Unexpected closing bracket found';
+ sUnterminatedLink = 'Unterminated link';
+ sEmptyLink = 'Empty link definition';
+ sWrongBracketOrder = 'Close bracket preceeds link open bracket';
+ sMultipleOpeners = 'More than one open bracket is present';
+ sMultipleClosers = 'More than one close bracket is present';
+ sNoURL = 'No URL specified';
+var
+ OpenBracketPos: Integer; // position of opening bracket in markup
+ CloseBracketPos: Integer; // position of closing bracket in markup
+ Prefix, Postfix: string; // text before and after link (can be empty)
+ LinkText: string; // link text
+begin
+ // Find open and closing brackets that delimit link text
+ OpenBracketPos := StrPos(cOpenBracket, Markup);
+ CloseBracketPos := StrPos(cCloseBracket, Markup);
+ if OpenBracketPos = 0 then
+ begin
+ // No links: plain text only
+ // check for errors
+ if CloseBracketPos > 0 then
+ raise EActiveTextParserError.Create(sUnexpectedCloser);
+ // record text element
+ ActiveText.AddElem(TActiveTextFactory.CreateTextElem(Markup));
+ end
+ else
+ begin
+ // We have a potential link
+ // check for errors
+ if CloseBracketPos = 0 then
+ raise EActiveTextParserError.Create(sUnterminatedLink);
+ if CloseBracketPos = OpenBracketPos + 1 then
+ raise EActiveTextParserError.Create(sEmptyLink);
+ if CloseBracketPos < OpenBracketPos then
+ raise EActiveTextParserError.Create(sWrongBracketOrder);
+ if StrCountDelims(cOpenBracket, Markup) > 1 then
+ raise EActiveTextParserError.Create(sMultipleOpeners);
+ if StrCountDelims(cCloseBracket, Markup) > 1 then
+ raise EActiveTextParserError.Create(sMultipleClosers);
+ // must have a URL
+ if fURL = '' then
+ raise EActiveTextParserError.Create(sNoURL);
+ // get the various components
+ LinkText := StrSlice(
+ Markup, OpenBracketPos + 1, CloseBracketPos - OpenBracketPos - 1
+ );
+ Assert(LinkText <> '',
+ ClassName + '.Parse: Link text is '' but has passed check');
+ Prefix := StrSliceLeft(Markup, OpenBracketPos - 1);
+ Postfix := StrSliceRight(Markup, Length(Markup) - CloseBracketPos);
+ // record the elements
+ if Prefix <> '' then
+ ActiveText.AddElem(TActiveTextFactory.CreateTextElem(Prefix));
+ ActiveText.AddElem(
+ TActiveTextFactory.CreateActionElem(
+ ekLink,
+ TActiveTextFactory.CreateAttrs(
+ TActiveTextAttr.Create(TActiveTextAttrNames.Link_URL, fURL)
+ ),
+ fsOpen
+ )
+ );
+ ActiveText.AddElem(TActiveTextFactory.CreateTextElem(LinkText));
+ ActiveText.AddElem(
+ TActiveTextFactory.CreateActionElem(
+ ekLink,
+ TActiveTextFactory.CreateAttrs(
+ TActiveTextAttr.Create(TActiveTextAttrNames.Link_URL, fURL)
+ ),
+ fsClose
+ )
+ );
+ if Postfix <> '' then
+ ActiveText.AddElem(TActiveTextFactory.CreateTextElem(Postfix));
+ end;
end;
end.
From bbaca9d24497bc07e9e6d1a5bab67f4e984d0ce5 Mon Sep 17 00:00:00 2001
From: delphidabbler <5164283+delphidabbler@users.noreply.github.com>
Date: Fri, 7 Apr 2023 00:03:31 +0100
Subject: [PATCH 032/189] Fix bugs in rendering of source code comments
Revised TActiveTextTextRenderer and made more customisable.
Heavily revised code used in TSourceComments to generate source code
comments from snippet description active text. Also increased indent
size.
Update UTextSnippetDoc re change in TActiveTextTextRenderer & increased
indent size.
---
Src/ActiveText.UTextRenderer.pas | 85 ++++++++++++++------------------
Src/USourceGen.pas | 54 ++++++++++++--------
Src/UTextSnippetDoc.pas | 5 +-
3 files changed, 75 insertions(+), 69 deletions(-)
diff --git a/Src/ActiveText.UTextRenderer.pas b/Src/ActiveText.UTextRenderer.pas
index c060188e6..78c90800f 100644
--- a/Src/ActiveText.UTextRenderer.pas
+++ b/Src/ActiveText.UTextRenderer.pas
@@ -21,23 +21,21 @@ interface
type
TActiveTextTextRenderer = class(TObject)
- public
+ strict private
const
/// Special space character used to indicate the start of a list
/// item.
/// This special character is a necessary kludge because some
- /// c odethat renders active text as formatted plain text strips away
+ /// code that renders active text as formatted plain text strips away
/// leading #32 characters as part of the formatting process. Therefore
/// indentation in list items is lost if #32 characters are used for it.
- /// NBSP was chosen since it should render the same as a space if calling
- /// code doesn't convert it.
+ /// NBSP was chosen since it should render the same as a space if not
+ /// removed.
LISpacer = NBSP; // Do not localise. Must be <> #32
/// Bullet character used when rendering unordered list items.
///
Bullet = '*'; // Do not localise. Must be <> #32 and <> LISpacer
- strict private
- const
- IndentDelta = 2;
+ DefaultIndentDelta = 2;
type
TListKind = (lkNumber, lkBullet);
TListState = record
@@ -60,6 +58,7 @@ TLIState = record
fIndent: UInt16;
fInPara: Boolean;
fInListItem: Boolean;
+ fIndentDelta: UInt8;
function CanEmitInline: Boolean;
procedure AppendToPara(const AText: string);
procedure InitialiseRender;
@@ -75,9 +74,10 @@ TLIState = record
destructor Destroy; override;
property DisplayURLs: Boolean read fDisplayURLs write fDisplayURLs
default False;
- function RenderWrapped(ActiveText: IActiveText; const PageWidth, LMargin,
- ParaOffset: Cardinal; const Prefix: string = '';
- const Suffix: string = ''): string;
+ property IndentDelta: UInt8 read fIndentDelta write fIndentDelta
+ default DefaultIndentDelta;
+ function RenderWrapped(ActiveText: IActiveText; const PageWidth,
+ LMargin: Cardinal): string;
end;
@@ -122,6 +122,7 @@ constructor TActiveTextTextRenderer.Create;
fIndent := 0;
fInPara := False;
fInListItem := False;
+ fIndentDelta := DefaultIndentDelta;
end;
destructor TActiveTextTextRenderer.Destroy;
@@ -200,6 +201,21 @@ function TActiveTextTextRenderer.Render(ActiveText: IActiveText): string;
procedure TActiveTextTextRenderer.RenderBlockActionElem(
Elem: IActiveTextActionElem);
+
+ procedure OpenListContainer(const ListKind: TListKind);
+ begin
+ if (fListStack.Count > 0) and (fInPara) then
+ OutputParagraph;
+ fListStack.Push(TListState.Create(ListKind));
+ Inc(fIndent, IndentDelta);
+ end;
+
+ procedure AddListMarker(const Marker: string);
+ begin
+ fParaBuilder.Append(Marker);
+ fParaBuilder.Append(StringOfChar(NBSP, IndentDelta - Length(Marker)));
+ end;
+
var
ListState: TListState;
begin
@@ -208,22 +224,12 @@ procedure TActiveTextTextRenderer.RenderBlockActionElem(
begin
fBlocksStack.Push(Elem.Kind);
case Elem.Kind of
- ekPara: {Do nothing} ;
- ekHeading: {Do nothing} ;
+ ekPara, ekHeading, ekBlock:
+ {Do nothing} ;
ekUnorderedList:
- begin
- if (fListStack.Count > 0) and (fInPara) then
- OutputParagraph;
- fListStack.Push(TListState.Create(lkBullet));
- Inc(fIndent, IndentDelta);
- end;
+ OpenListContainer(lkBullet);
ekOrderedList:
- begin
- if (fListStack.Count > 0) and (fInPara) then
- OutputParagraph;
- fListStack.Push(TListState.Create(lkNumber));
- Inc(fIndent, IndentDelta);
- end;
+ OpenListContainer(lkNumber);
ekListItem:
begin
// Update list number of current list
@@ -235,16 +241,9 @@ procedure TActiveTextTextRenderer.RenderBlockActionElem(
// Act depending on current list kind
case fListStack.Peek.ListKind of
lkNumber:
- begin
- // Number list: start a new numbered item, with current number
- fParaBuilder.Append(IntToStr(fListStack.Peek.ListNumber));
- fParaBuilder.Append(NBSP);
- end;
+ AddListMarker(IntToStr(fListStack.Peek.ListNumber));
lkBullet:
- begin
- // Bullet list: start a new bullet point
- fParaBuilder.Append(Bullet + NBSP);
- end;
+ AddListMarker(Bullet);
end;
end;
end;
@@ -252,17 +251,9 @@ procedure TActiveTextTextRenderer.RenderBlockActionElem(
fsClose:
begin
case Elem.Kind of
- ekPara:
+ ekPara, ekHeading, ekBlock:
OutputParagraph;
- ekHeading:
- OutputParagraph;
- ekUnorderedList:
- begin
- OutputParagraph;
- fListStack.Pop;
- Dec(fIndent, IndentDelta);
- end;
- ekOrderedList:
+ ekUnorderedList, ekOrderedList:
begin
OutputParagraph;
fListStack.Pop;
@@ -315,7 +306,7 @@ procedure TActiveTextTextRenderer.RenderURL(Elem: IActiveTextActionElem);
end;
function TActiveTextTextRenderer.RenderWrapped(ActiveText: IActiveText;
- const PageWidth, LMargin, ParaOffset: Cardinal; const Prefix, Suffix: string):
+ const PageWidth, LMargin: Cardinal):
string;
var
Paras: IStringList;
@@ -367,13 +358,13 @@ function TActiveTextTextRenderer.RenderWrapped(ActiveText: IActiveText;
begin
Result := '';
- Paras := TIStringList.Create(Prefix + Render(ActiveText) + Suffix, EOL, True);
+ Paras := TIStringList.Create(Render(ActiveText), EOL, True);
for Para in Paras do
begin
if IsListItem then
begin
- Offset := -ParaOffset;
- ParaIndent := CalcParaIndent + LMargin + ParaOffset;
+ Offset := -IndentDelta;
+ ParaIndent := CalcParaIndent + LMargin + IndentDelta;
end
else
begin
diff --git a/Src/USourceGen.pas b/Src/USourceGen.pas
index 3d10b57ef..ccf94698a 100644
--- a/Src/USourceGen.pas
+++ b/Src/USourceGen.pas
@@ -41,7 +41,7 @@ TSourceComments = class(TNoConstructObject)
/// maximum width indented by the given number of spaces on the left,
/// optionally truncated to the first paragraph.
class function FormatActiveTextCommentInner(ActiveText: IActiveText;
- const Indent: Cardinal; const Truncate: Boolean): string;
+ const LineWidth: Cardinal; const Truncate: Boolean): string;
public
/// Returns a description of the given comment style.
@@ -60,7 +60,7 @@ TSourceComments = class(TNoConstructObject)
/// string.Formatted comment or empty string if Style = csNone.
///
class function FormatSnippetComment(const Style: TCommentStyle;
- const TruncateComments: Boolean; const Text: IActiveText): string;
+ const TruncateComments: Boolean; Text: IActiveText): string;
/// Formats document's header text as a Pascal comment.
/// IStringList [in] List of paragraphs of header
@@ -259,9 +259,11 @@ implementation
const
/// Maximum number of characters on a source code line.
cLineWidth = 80;
-const
/// Size of indenting used for source code, in characters.
cIndent = 2;
+ /// Size of indenting used for rendering comments from active text.
+ ///
+ cCommentIndent = 4;
type
@@ -1137,11 +1139,13 @@ class function TSourceComments.CommentStyleDesc(
end;
class function TSourceComments.FormatActiveTextCommentInner(
- ActiveText: IActiveText; const Indent: Cardinal; const Truncate: Boolean):
- string;
+ ActiveText: IActiveText; const LineWidth: Cardinal;
+ const Truncate: Boolean): string;
var
Renderer: TActiveTextTextRenderer;
ProcessedActiveText: IActiveText;
+ Lines: IStringList;
+ Line: string;
begin
if Truncate then
ProcessedActiveText := ActiveText.FirstBlock
@@ -1150,9 +1154,17 @@ class function TSourceComments.FormatActiveTextCommentInner(
Renderer := TActiveTextTextRenderer.Create;
try
Renderer.DisplayURLs := False;
- Result := Renderer.RenderWrapped(
- ProcessedActiveText, cLineWidth, Indent, Indent
+ Renderer.IndentDelta := cCommentIndent;
+ Result := '';
+ Lines := TIStringList.Create(
+ Renderer.RenderWrapped(ProcessedActiveText, LineWidth, 0),
+ EOL,
+ True,
+ False
);
+ for Line in Lines do
+ Result := Result + StringOfChar(' ', cLineWidth - LineWidth) + Line + EOL;
+ Result := StrTrimRight(Result);
finally
Renderer.Free;
end;
@@ -1189,25 +1201,27 @@ class function TSourceComments.FormatHeaderComments(
end;
class function TSourceComments.FormatSnippetComment(const Style: TCommentStyle;
- const TruncateComments: Boolean; const Text: IActiveText): string;
+ const TruncateComments: Boolean; Text: IActiveText): string;
begin
case Style of
csNone:
Result := '';
csBefore:
- Result := '{'
- + EOL
- + FormatActiveTextCommentInner(Text, cIndent, TruncateComments)
- + EOL
- + '}';
+ begin
+ Result := '{' + EOL
+ + FormatActiveTextCommentInner(
+ Text, cLineWidth - cIndent, TruncateComments
+ )
+ + EOL + '}';
+ end;
csAfter:
- Result := StrOfChar(TActiveTextTextRenderer.LISpacer, cIndent)
- + '{'
- + EOL
- + FormatActiveTextCommentInner(Text, 2 * cIndent, TruncateComments)
- + EOL
- + StrOfChar(TActiveTextTextRenderer.LISpacer, cIndent)
- + '}';
+ begin
+ Result := StrOfChar(' ', cIndent) + '{' + EOL
+ + FormatActiveTextCommentInner(
+ Text, cLineWidth - 2 * cIndent, TruncateComments
+ )
+ + EOL + StringOfChar(' ', cIndent) + '}';
+ end;
end;
end;
diff --git a/Src/UTextSnippetDoc.pas b/Src/UTextSnippetDoc.pas
index 87e72c4dd..d046c38c0 100644
--- a/Src/UTextSnippetDoc.pas
+++ b/Src/UTextSnippetDoc.pas
@@ -36,7 +36,7 @@ TTextSnippetDoc = class(TSnippetDoc)
cPageWidth = 80;
/// Size of a single level of indentation in characters.
///
- cIndent = 2;
+ cIndent = 4;
strict private
/// Renders given active text as word-wrapped paragraphs of width
/// cPageWidth.
@@ -111,8 +111,9 @@ procedure TTextSnippetDoc.RenderActiveText(ActiveText: IActiveText);
Renderer := TActiveTextTextRenderer.Create;
try
Renderer.DisplayURLs := True;
+ Renderer.IndentDelta := cIndent;
fWriter.WriteLine(
- Renderer.RenderWrapped(ActiveText, cPageWidth, 0, cIndent)
+ Renderer.RenderWrapped(ActiveText, cPageWidth, 0)
);
finally
Renderer.Free;
From 2d3be989b2c2301eb1c37f7a5f02d1c4e2168280 Mon Sep 17 00:00:00 2001
From: delphidabbler <5164283+delphidabbler@users.noreply.github.com>
Date: Fri, 7 Apr 2023 23:10:00 +0100
Subject: [PATCH 033/189] Support rendering lists in snippet RTF reports
Update URTFUtils to support 3 more RTF controls for controlling first
line offset, left indent and tabstops.
Add support to TRTFStyles for recording indentation and calculating the
identation required at a given identation depth.
Add new methods to TRTFBuilder to set indents and tab stops. (Also did
minor refactoring of SetParaSpacing method.)
Major rewrite of TActiveTextRTF to support lists. Added support for
rendering active text bullet and numbered lists in RTF. Also added
support for new ekBlock active text element.
Add styles for lists in TRTFSnippetDoc and adjusted some other styles to
suit. Also evised spacing between description and source code & added
"Source Code" heading. Updated and added some constants.
---
Src/ActiveText.URTFRenderer.pas | 220 ++++++++++++++++++++++++++++++--
Src/URTFBuilder.pas | 31 ++++-
Src/URTFSnippetDoc.pas | 123 +++++++++++++-----
Src/URTFStyles.pas | 36 +++++-
Src/URTFUtils.pas | 10 +-
5 files changed, 360 insertions(+), 60 deletions(-)
diff --git a/Src/ActiveText.URTFRenderer.pas b/Src/ActiveText.URTFRenderer.pas
index dc0267b79..44564d3b8 100644
--- a/Src/ActiveText.URTFRenderer.pas
+++ b/Src/ActiveText.URTFRenderer.pas
@@ -45,11 +45,34 @@ TActiveTextRTFStyleMap = class(TObject)
type
TActiveTextRTF = class(TObject)
strict private
+ const
+ // Difference between indent levels in twips
+ IndentDelta = 360;
+ // RTF Bullet character
+ Bullet = #$2022;
+ type
+ TListKind = (lkNumber, lkBullet);
+ TListState = record
+ public
+ ListNumber: Cardinal;
+ ListKind: TListKind;
+ constructor Create(AListKind: TListKind);
+ end;
+ TLIState = record
+ IsFirstPara: Boolean;
+ Prefix: string;
+ constructor Create(AIsFirstPara: Boolean; const APrefix: string);
+ end;
var
fElemStyleMap: TActiveTextRTFStyleMap;
fDisplayURLs: Boolean;
fURLStyle: TRTFStyle;
- fInBlock: Boolean;
+ fBlockStack: TStack;
+ fListStack: TStack;
+ fIndentStack: TStack;
+ fLIStack: TStack;
+ fIndentLevel: Byte; // logical indent level
+ fInPara: Boolean;
procedure SetElemStyleMap(const ElemStyleMap: TActiveTextRTFStyleMap);
procedure Initialise(const Builder: TRTFBuilder);
procedure RenderTextElem(Elem: IActiveTextTextElem;
@@ -60,6 +83,7 @@ TActiveTextRTF = class(TObject)
const Builder: TRTFBuilder);
procedure RenderURL(Elem: IActiveTextActionElem;
const Builder: TRTFBuilder);
+ function CanEmitInline: Boolean;
public
constructor Create;
destructor Destroy; override;
@@ -76,8 +100,10 @@ implementation
uses
+ // Delphi
+ SysUtils, Generics.Defaults,
// Project
- SysUtils, Generics.Defaults;
+ UConsts, UStrUtils;
{ TActiveTextRTFStyleMap }
@@ -155,15 +181,32 @@ procedure TActiveTextRTFStyleMap.MakeMonochrome;
{ TActiveTextRTF }
+function TActiveTextRTF.CanEmitInline: Boolean;
+begin
+ if fBlockStack.Count <= 0 then
+ Exit(False);
+ Result := TActiveTextElemCaps.CanContainText(fBlockStack.Peek);
+end;
+
constructor TActiveTextRTF.Create;
begin
inherited Create;
fElemStyleMap := TActiveTextRTFStyleMap.Create;
fURLStyle := TRTFStyle.CreateNull;
+ fBlockStack := TStack.Create;
+ fListStack := TStack.Create;
+ fIndentStack := TStack.Create;
+ fLIStack := TStack.Create;
+ fIndentLevel := 0;
+ fInPara := False;
end;
destructor TActiveTextRTF.Destroy;
begin
+ fLIStack.Free;
+ fIndentStack.Free;
+ fListStack.Free;
+ fBlockStack.Free;
fElemStyleMap.Free;
inherited;
end;
@@ -189,7 +232,6 @@ procedure TActiveTextRTF.Render(ActiveText: IActiveText;
ActionElem: IActiveTextActionElem;
begin
Initialise(RTFBuilder);
- fInBlock := False;
for Elem in ActiveText do
begin
if Supports(Elem, IActiveTextTextElem, TextElem) then
@@ -206,19 +248,146 @@ procedure TActiveTextRTF.Render(ActiveText: IActiveText;
procedure TActiveTextRTF.RenderBlockActionElem(Elem: IActiveTextActionElem;
const Builder: TRTFBuilder);
+
+ procedure OpenListContainer(const ListKind: TListKind);
+ begin
+ fListStack.Push(TListState.Create(ListKind));
+ Inc(fIndentLevel);
+ Builder.BeginGroup;
+ end;
+
+ function IndentTwips: SmallInt;
+ begin
+ Result := fElemStyleMap[ekListItem].IndentLevelToTwips(fIndentLevel)
+ end;
+
+var
+ ListState: TListState;
+ LIState: TLIState;
+ Style: TRTFStyle;
begin
case Elem.State of
fsOpen:
begin
- fInBlock := True;
- Builder.BeginGroup;
- Builder.ApplyStyle(fElemStyleMap[Elem.Kind]);
+ fInPara := False;
+ fBlockStack.Push(Elem.Kind);
+ case Elem.Kind of
+ ekPara, ekHeading, ekBlock:
+ begin
+ Builder.BeginGroup;
+ Style := fElemStyleMap[Elem.Kind];
+ if fLIStack.Count > 0 then
+ begin
+ Builder.SetTabStops([IndentTwips]);
+ if fLIStack.Peek.IsFirstPara then
+ begin
+ Builder.SetIndents(
+ IndentTwips, -fElemStyleMap[ekListItem].IndentDelta
+ );
+ if (fListStack.Count > 0) then
+ begin
+ if fListStack.Peek.ListNumber = 1 then
+ begin
+ Style.Capabilities := Style.Capabilities + [scParaSpacing];
+ if fListStack.Peek.ListKind = lkNumber then
+ Style.ParaSpacing := TRTFParaSpacing.Create(
+ fElemStyleMap[ekOrderedList].ParaSpacing.Before, 0.0
+ )
+ else
+ Style.ParaSpacing := TRTFParaSpacing.Create(
+ fElemStyleMap[ekUnorderedList].ParaSpacing.Before, 0.0
+ )
+ end
+ else if fListStack.Peek.ListNumber > 1 then
+ begin
+ if Elem.Kind = ekHeading then
+ begin
+ Style.Capabilities := Style.Capabilities + [scParaSpacing];
+ Style.ParaSpacing := fElemStyleMap[ekPara].ParaSpacing;
+ end;
+ end;
+ end;
+ Builder.ApplyStyle(Style);
+ Builder.AddText(fLIStack.Peek.Prefix);
+ Builder.AddText(TAB);
+ fInPara := True;
+ end
+ else
+ begin
+ Builder.ApplyStyle(Style);
+ Builder.SetIndents(IndentTwips, 0);
+ end;
+ end
+ else
+ begin
+ Builder.ApplyStyle(Style);
+ Builder.SetIndents(IndentTwips, 0);
+ end;
+ end;
+ ekUnorderedList:
+ OpenListContainer(lkBullet);
+ ekOrderedList:
+ OpenListContainer(lkNumber);
+ ekListItem:
+ begin
+ // Update list number of current list
+ ListState := fListStack.Pop;
+ Inc(ListState.ListNumber, 1);
+ fListStack.Push(ListState);
+ Builder.BeginGroup;
+ Builder.ApplyStyle(fElemStyleMap[Elem.Kind]);
+ case fListStack.Peek.ListKind of
+ lkNumber:
+ begin
+ fLIStack.Push(
+ TLIState.Create(
+ True, IntToStr(fListStack.Peek.ListNumber) + '.'
+ )
+ );
+ end;
+ lkBullet:
+ begin
+ fLIStack.Push(TLIState.Create(True, Bullet));
+ end;
+ end;
+ Builder.ClearParaFormatting;
+ end;
+ end;
end;
fsClose:
begin
- Builder.EndPara;
- Builder.EndGroup;
- fInBlock := False;
+ case Elem.Kind of
+ ekPara, ekHeading, ekBlock:
+ begin
+ if (fLIStack.Count > 0) and (fLIStack.Peek.IsFirstPara) then
+ begin
+ // Update item at top of LI stack to record not first para
+ LIState := fLIStack.Pop;
+ LIState.IsFirstPara := False;
+ fLIStack.Push(LIState);
+ end;
+ if fInPara then
+ Builder.EndPara;
+ Builder.EndGroup;
+ end;
+ ekUnorderedList, ekOrderedList:
+ begin
+ if fInPara then
+ Builder.EndPara;
+ Builder.EndGroup;
+ fListStack.Pop;
+ Dec(fIndentLevel);
+ end;
+ ekListItem:
+ begin
+ if fInPara then
+ Builder.EndPara;
+ Builder.EndGroup;
+ fLIStack.Pop;
+ end;
+ end;
+ fBlockStack.Pop;
+ fInPara := False;
end;
end;
end;
@@ -226,7 +395,7 @@ procedure TActiveTextRTF.RenderBlockActionElem(Elem: IActiveTextActionElem;
procedure TActiveTextRTF.RenderInlineActionElem(Elem: IActiveTextActionElem;
const Builder: TRTFBuilder);
begin
- if not fInBlock then
+ if not CanEmitInline then
Exit;
case Elem.State of
fsOpen:
@@ -245,10 +414,20 @@ procedure TActiveTextRTF.RenderInlineActionElem(Elem: IActiveTextActionElem;
procedure TActiveTextRTF.RenderTextElem(Elem: IActiveTextTextElem;
const Builder: TRTFBuilder);
+var
+ TheText: string;
begin
- if not fInBlock then
+ if not CanEmitInline then
Exit;
- Builder.AddText(Elem.Text);
+ TheText := Elem.Text;
+ // no white space emitted after block start until 1st non-white space
+ // character encountered
+ if not fInPara then
+ TheText := StrTrimLeft(Elem.Text);
+ if TheText = '' then
+ Exit;
+ Builder.AddText(TheText);
+ fInPara := True;
end;
procedure TActiveTextRTF.RenderURL(Elem: IActiveTextActionElem;
@@ -271,5 +450,22 @@ procedure TActiveTextRTF.SetElemStyleMap(
fElemStyleMap.Assign(ElemStyleMap);
end;
+{ TActiveTextRTF.TListState }
+
+constructor TActiveTextRTF.TListState.Create(AListKind: TListKind);
+begin
+ ListNumber := 0;
+ ListKind := AListKind;
+end;
+
+{ TActiveTextRTF.TLIState }
+
+constructor TActiveTextRTF.TLIState.Create(AIsFirstPara: Boolean;
+ const APrefix: string);
+begin
+ IsFirstPara := AIsFirstPara;
+ Prefix := APrefix;
+end;
+
end.
diff --git a/Src/URTFBuilder.pas b/Src/URTFBuilder.pas
index f25efaf82..64e8cf664 100644
--- a/Src/URTFBuilder.pas
+++ b/Src/URTFBuilder.pas
@@ -179,6 +179,12 @@ TRTFBuilder = class(TObject)
/// Sets before and after spacing, in points, to be used for
/// subsequent paragraphs.
procedure SetParaSpacing(const Spacing: TRTFParaSpacing);
+ /// Sets left and first line indents, in twips to be used for
+ /// subsequent paragraphs.
+ procedure SetIndents(const LeftIndent, FirstLineOffset: SmallInt);
+ /// Sets tab stops, in twips, to be used for subsequent
+ /// paragraphs.
+ procedure SetTabStops(const TabStops: array of SmallInt);
/// Sets paragraph and character styling for subsequent text
/// according to given RTF style.
procedure ApplyStyle(const Style: TRTFStyle);
@@ -354,11 +360,30 @@ procedure TRTFBuilder.SetFontStyle(const Style: TFontStyles);
AddControl(RTFControl(rcUnderline));
end;
+procedure TRTFBuilder.SetIndents(const LeftIndent, FirstLineOffset: SmallInt);
+begin
+ AddControl(RTFControl(rcLeftIndent, LeftIndent));
+ AddControl(RTFControl(rcFirstLineOffset, FirstLineOffset));
+end;
+
procedure TRTFBuilder.SetParaSpacing(const Spacing: TRTFParaSpacing);
+const
+ TwipsPerPoint = 20; // Note: 20 Twips in a point
+begin
+ AddControl(
+ RTFControl(rcSpaceBefore, FloatToInt(TwipsPerPoint * Spacing.Before))
+ );
+ AddControl(
+ RTFControl(rcSpaceAfter, FloatToInt(TwipsPerPoint * Spacing.After))
+ );
+end;
+
+procedure TRTFBuilder.SetTabStops(const TabStops: array of SmallInt);
+var
+ Tab: SmallInt;
begin
- // Note: 20 Twips in a point
- AddControl(RTFControl(rcSpaceBefore, FloatToInt(20 * Spacing.Before)));
- AddControl(RTFControl(rcSpaceAfter, FloatToInt(20 * Spacing.After)));
+ for Tab in TabStops do
+ AddControl(RTFControl(rcTabStop, Tab));
end;
{ TRTFFontTable }
diff --git a/Src/URTFSnippetDoc.pas b/Src/URTFSnippetDoc.pas
index 375b10213..d947e4c01 100644
--- a/Src/URTFSnippetDoc.pas
+++ b/Src/URTFSnippetDoc.pas
@@ -39,8 +39,11 @@ TRTFSnippetDoc = class(TSnippetDoc)
fBuilder: TRTFBuilder;
/// Flag indicates whether to output in colour.
fUseColour: Boolean;
-
+ /// Styles to apply to snippet description active text.
+ ///
fDescStyles: TActiveTextRTFStyleMap;
+ /// Styles to apply to snippet extra information active text.
+ ///
fExtraStyles: TActiveTextRTFStyleMap;
/// Styling applied to URLs.
fURLStyle: TRTFStyle;
@@ -49,14 +52,24 @@ TRTFSnippetDoc = class(TSnippetDoc)
MainFontName = 'Tahoma';
/// Name of mono font.
MonoFontName = 'Courier New';
- /// Size of heading font.
- HeadingFontSize = 16;
- /// Size of paragraph font.
+ /// Size of font used for database information in points.
+ ///
+ DBInfoFontSize = 9; // points
+ /// Size of heading font in points.
+ HeadingFontSize = 16; // points
+ /// Size of sub-heading font in points.
+ /// Used in descripton and extra active text.
+ SubHeadingFontSize = 12;
+ /// Size of paragraph font in points.
ParaFontSize = 10;
/// Paragraph spacing in points.
- ParaSpacing = 12.0;
- /// Size of font used for database information.
- DBInfoFontSize = 9;
+ ParaSpacing = 6.0;
+ /// Spacing for non-paragrap blocks in points.
+ NoParaBlockSpacing = 0.0;
+ /// Spacing of list blocks in points.
+ ListSpacing = ParaSpacing;
+ /// Step size of indents and tabs in twips.
+ IndentDelta = TRTFStyle.DefaultIndentDelta;
strict private
/// Initialises RTF style used when rendering active text as RTF.
///
@@ -167,43 +180,71 @@ procedure TRTFSnippetDoc.InitStyles;
[scColour], TRTFFont.CreateNull, 0.0, [], clExternalLink
);
- fExtraStyles.Add(
- ekPara,
- TRTFStyle.Create(
- TRTFParaSpacing.Create(ParaSpacing, 0.0)
- )
- );
+ // Active text styles
+
+ // -- Active text block styles
+
fDescStyles.Add(
- ekPara,
- TRTFStyle.Create(
- TRTFParaSpacing.Create(0.0, ParaSpacing)
- )
+ ekHeading,
+ TRTFStyle.Create(
+ [scParaSpacing, scFontStyles, scFontSize],
+ TRTFParaSpacing.Create(0.0, 0.0),
+ TRTFFont.CreateNull,
+ SubHeadingFontSize,
+ [fsBold],
+ clNone
+ )
);
-
fExtraStyles.Add(
ekHeading,
TRTFStyle.Create(
- [scParaSpacing, scFontStyles],
+ [scParaSpacing, scFontStyles, scFontSize],
TRTFParaSpacing.Create(ParaSpacing, 0.0),
TRTFFont.CreateNull,
- 0.0,
+ SubHeadingFontSize,
[fsBold],
clNone
)
);
+
fDescStyles.Add(
- ekHeading,
+ ekPara,
+ TRTFStyle.Create(TRTFParaSpacing.Create(ParaSpacing, 0.0))
+ );
+ fExtraStyles.Add(ekPara, fDescStyles[ekPara]);
+
+ fDescStyles.Add(
+ ekBlock,
+ TRTFStyle.Create(TRTFParaSpacing.Create(NoParaBlockSpacing, 0.0))
+ );
+ fExtraStyles.Add(ekBlock, fDescStyles[ekBlock]);
+
+ fDescStyles.Add(
+ ekUnorderedList,
+ TRTFStyle.Create(TRTFParaSpacing.Create(ListSpacing, 0.0))
+ );
+ fExtraStyles.Add(ekUnorderedList, fDescStyles[ekUnorderedList]);
+
+ fDescStyles.Add(ekOrderedList, fDescStyles[ekUnorderedList]);
+ fExtraStyles.Add(ekOrderedList, fDescStyles[ekOrderedList]);
+
+ fDescStyles.Add(
+ ekListItem,
TRTFStyle.Create(
- [scParaSpacing, scFontStyles],
- TRTFParaSpacing.Create(0.0, ParaSpacing),
+ [scIndentDelta],
+ TRTFParaSpacing.CreateNull,
TRTFFont.CreateNull,
0.0,
- [fsBold],
- clNone
+ [],
+ clNone,
+ 360
)
);
+ fExtraStyles.Add(ekListItem, fDescStyles[ekListItem]);
- fExtraStyles.Add(
+ // -- Active text inline styles
+
+ fDescStyles.Add(
ekStrong,
TRTFStyle.Create(
[scFontStyles],
@@ -213,9 +254,9 @@ procedure TRTFSnippetDoc.InitStyles;
clNone
)
);
- fDescStyles.Add(ekStrong, fExtraStyles[ekStrong]);
+ fExtraStyles.Add(ekStrong, fDescStyles[ekStrong]);
- fExtraStyles.Add(
+ fDescStyles.Add(
ekEm,
TRTFStyle.Create(
[scFontStyles],
@@ -225,9 +266,9 @@ procedure TRTFSnippetDoc.InitStyles;
clNone
)
);
- fDescStyles.Add(ekEm, fExtraStyles[ekEm]);
+ fExtraStyles.Add(ekEm, fDescStyles[ekEm]);
- fExtraStyles.Add(
+ fDescStyles.Add(
ekVar,
TRTFStyle.Create(
[scFontStyles, scColour],
@@ -237,9 +278,9 @@ procedure TRTFSnippetDoc.InitStyles;
clVarText
)
);
- fDescStyles.Add(ekVar, fExtraStyles[ekVar]);
+ fExtraStyles.Add(ekVar, fDescStyles[ekVar]);
- fExtraStyles.Add(
+ fDescStyles.Add(
ekWarning,
TRTFStyle.Create(
[scFontStyles, scColour],
@@ -249,9 +290,9 @@ procedure TRTFSnippetDoc.InitStyles;
clWarningText
)
);
- fDescStyles.Add(ekWarning, fExtraStyles[ekWarning]);
+ fExtraStyles.Add(ekWarning, fDescStyles[ekWarning]);
- fExtraStyles.Add(
+ fDescStyles.Add(
ekMono,
TRTFStyle.Create(
[scFont],
@@ -261,7 +302,9 @@ procedure TRTFSnippetDoc.InitStyles;
clNone
)
);
- fDescStyles.Add(ekMono, fExtraStyles[ekMono]);
+ fExtraStyles.Add(ekMono, fDescStyles[ekMono]);
+
+ // Fixes for monochrome
if not fUseColour then
begin
@@ -356,7 +399,17 @@ procedure TRTFSnippetDoc.RenderHeading(const Heading: string;
procedure TRTFSnippetDoc.RenderSourceCode(const SourceCode: string);
var
Renderer: IHiliteRenderer; // renders highlighted source as RTF
+resourcestring
+ sHeading = 'Source Code:';
begin
+ fBuilder.ResetCharStyle;
+ fBuilder.SetFont(MainFontName);
+ fBuilder.SetFontSize(ParaFontSize);
+ fBuilder.SetFontStyle([fsBold]);
+ fBuilder.SetParaSpacing(TRTFParaSpacing.Create(ParaSpacing, ParaSpacing));
+ fBuilder.AddText(sHeading);
+ fBuilder.ResetCharStyle;
+ fBuilder.EndPara;
fBuilder.ClearParaFormatting;
Renderer := TRTFHiliteRenderer.Create(fBuilder, fHiliteAttrs);
TSyntaxHiliter.Hilite(SourceCode, Renderer);
diff --git a/Src/URTFStyles.pas b/Src/URTFStyles.pas
index 6142e8762..60e5dd89a 100644
--- a/Src/URTFStyles.pas
+++ b/Src/URTFStyles.pas
@@ -88,7 +88,8 @@ TRTFParaSpacing = record
scFont,
scFontSize,
scFontStyles,
- scColour
+ scColour,
+ scIndentDelta
);
type
@@ -97,24 +98,30 @@ TRTFParaSpacing = record
type
TRTFStyle = record
public
+ const
+ DefaultIndentDelta = 360;
var
ParaSpacing: TRTFParaSpacing;
Font: TRTFFont;
FontSize: Double;
FontStyles: TFontStyles;
Colour: TColor;
+ IndentDelta: SmallInt;
Capabilities: TRTFStyleCaps;
constructor Create(const ACapabilities: TRTFStyleCaps;
const AParaSpacing: TRTFParaSpacing; const AFont: TRTFFont;
const AFontSize: Double; const AFontStyles: TFontStyles;
- const AColour: TColor); overload;
+ const AColour: TColor; const AIndentDelta: SmallInt = DefaultIndentDelta);
+ overload;
constructor Create(const ACapabilities: TRTFStyleCaps;
const AFont: TRTFFont; const AFontSize:
- Double; const AFontStyles: TFontStyles; const AColour: TColor); overload;
+ Double; const AFontStyles: TFontStyles; const AColour: TColor;
+ const AIndentDelta: SmallInt = DefaultIndentDelta); overload;
constructor Create(const AParaSpacing: TRTFParaSpacing); overload;
class function CreateNull: TRTFStyle; static;
function IsNull: Boolean;
procedure MakeMonochrome;
+ function IndentLevelToTwips(const ALevel: Byte): SmallInt;
class operator Equal(const Left, Right: TRTFStyle): Boolean;
class operator NotEqual(const Left, Right: TRTFStyle): Boolean;
end;
@@ -186,7 +193,7 @@ class function TRTFParaSpacing.CreateNull: TRTFParaSpacing;
constructor TRTFStyle.Create(const ACapabilities: TRTFStyleCaps;
const AParaSpacing: TRTFParaSpacing; const AFont: TRTFFont;
const AFontSize: Double; const AFontStyles: TFontStyles;
- const AColour: TColor);
+ const AColour: TColor; const AIndentDelta: SmallInt);
begin
Capabilities := ACapabilities;
ParaSpacing := AParaSpacing;
@@ -194,11 +201,13 @@ constructor TRTFStyle.Create(const ACapabilities: TRTFStyleCaps;
FontSize := AFontSize;
FontStyles := AFontStyles;
Colour := AColour;
+ IndentDelta := AIndentDelta;
end;
constructor TRTFStyle.Create(const ACapabilities: TRTFStyleCaps;
const AFont: TRTFFont; const AFontSize: Double;
- const AFontStyles: TFontStyles; const AColour: TColor);
+ const AFontStyles: TFontStyles; const AColour: TColor;
+ const AIndentDelta: SmallInt);
begin
Create(
ACapabilities - [scParaSpacing],
@@ -206,7 +215,8 @@ constructor TRTFStyle.Create(const ACapabilities: TRTFStyleCaps;
AFont,
AFontSize,
AFontStyles,
- AColour
+ AColour,
+ AIndentDelta
);
end;
@@ -231,7 +241,19 @@ class function TRTFStyle.CreateNull: TRTFStyle;
and StrSameText(Left.Font.Name, Right.Font.Name)
and SameValue(Left.FontSize, Right.FontSize)
and (Left.FontStyles = Right.FontStyles)
- and (Left.Colour = Right.Colour);
+ and (Left.Colour = Right.Colour)
+ and (Left.IndentDelta = Right.IndentDelta);
+end;
+
+function TRTFStyle.IndentLevelToTwips(const ALevel: Byte): SmallInt;
+var
+ Delta: SmallInt;
+begin
+ if scIndentDelta in Capabilities then
+ Delta := IndentDelta
+ else
+ Delta := DefaultIndentDelta;
+ Result := ALevel * Delta;
end;
function TRTFStyle.IsNull: Boolean;
diff --git a/Src/URTFUtils.pas b/Src/URTFUtils.pas
index 927014dde..2b1834bc9 100644
--- a/Src/URTFUtils.pas
+++ b/Src/URTFUtils.pas
@@ -3,7 +3,7 @@
* v. 2.0. If a copy of the MPL was not distributed with this file, You can
* obtain one at https://mozilla.org/MPL/2.0/
*
- * Copyright (C) 2005-2021, Peter Johnson (gravatar.com/delphidabbler).
+ * Copyright (C) 2005-2022, Peter Johnson (gravatar.com/delphidabbler).
*
* Utility functions used when processing RTF.
}
@@ -66,7 +66,10 @@ interface
rcUnicodeChar, // defines a Unicode character as signed 16bit value
rcUnicodePair, // introduces pair of ANSI and Unicode destinations
rcUnicodeDest, // introduces Unicode destination
- rcIgnore // denotes following control can be ignored
+ rcIgnore, // denotes following control can be ignored
+ rcFirstLineOffset, // first line indent in twips (relative to \li)
+ rcLeftIndent, // left indent in twips
+ rcTabStop // sets a tab stop in twips
);
type
@@ -193,7 +196,8 @@ implementation
'rtf', 'ansi', 'ansicpg', 'deff', 'deflang', 'fonttbl', 'fprq', 'fcharset',
'fnil', 'froman', 'fswiss', 'fmodern', 'fscript', 'fdecor', 'ftech',
'colortbl', 'red', 'green', 'blue', 'info', 'title', 'pard', 'par', 'plain',
- 'f', 'cf', 'b', 'i', 'ul', 'fs', 'sb', 'sa', 'u', 'upr', 'ud', '*'
+ 'f', 'cf', 'b', 'i', 'ul', 'fs', 'sb', 'sa', 'u', 'upr', 'ud', '*',
+ 'fi', 'li', 'tx'
);
function RTFControl(const Ctrl: TRTFControl): ASCIIString;
From 45900e47d3a806c52b73230a6114abafecc90812 Mon Sep 17 00:00:00 2001
From: delphidabbler <5164283+delphidabbler@users.noreply.github.com>
Date: Sat, 8 Apr 2023 00:47:12 +0100
Subject: [PATCH 034/189] Correct and revise REML documentation
Updated REML design document and REML help topic
---
Docs/Design/reml.html | 25 ++++++++++++++-----------
Src/Help/HTML/reml.htm | 30 +++++++++++++++---------------
2 files changed, 29 insertions(+), 26 deletions(-)
diff --git a/Docs/Design/reml.html b/Docs/Design/reml.html
index 9762decaf..51fb91de4 100644
--- a/Docs/Design/reml.html
+++ b/Docs/Design/reml.html
@@ -279,19 +279,19 @@
-
-
<p>...</p>
– Renders the enclosed markup as a simple paragraph.
+ <p>...</p>
– Renders the enclosed markup as a simple paragraph.
-
<heading>...</heading>
– Renders the enclosed markup as a heading.
-
-
<ol>...</ol>
– Renders the enclosed HTML as an ordered list. Must contain <li>...</li>
blocks and nothing else.
+ <ol>...</ol>
– Renders the enclosed markup as an ordered list.
-
-
<ul>...</ul>
– Renders the enclosed HTML as an unordered list. Must contain <li>...</li>
blocks and nothing else.
+ <ul>...</ul>
– Renders the enclosed markup as an unordered list.
-
-
<li>...</li>
– Renders the enclosed HTML as a list item. Must only be used within <ol>...</ol>
and <ul>...</ul>
blocks.
+ <li>...</li>
– Renders the enclosed markup as a list item.
@@ -308,7 +308,7 @@
<ol>...</ol>
and <ul>...</ul>
blocks must only contain one or more <li>...</li>
blocks.