33 * v. 2.0. If a copy of the MPL was not distributed with this file, You can
44 * obtain one at https://mozilla.org/MPL/2.0/
55 *
6- * Copyright (C) 2012-2021 , Peter Johnson (gravatar.com/delphidabbler).
6+ * Copyright (C) 2012-2022 , Peter Johnson (gravatar.com/delphidabbler).
77 *
88 * Implements class that renders active text as plain text in fixed width, word
99 * wrapped paragraphs.
1515interface
1616
1717uses
18- SysUtils,
19- ActiveText.UMain;
18+ SysUtils, Generics.Collections,
19+ ActiveText.UMain,
20+ UConsts;
2021
2122type
2223 TActiveTextTextRenderer = class (TObject)
24+ public
25+ const
26+ // / <summary>Special space character used to indicate the start of a list
27+ // / item.</summary>
28+ // / <remarks>This special character is a necessary kludge because some
29+ // / c odethat renders active text as formatted plain text strips away
30+ // / leading #32 characters as part of the formatting process. Therefore
31+ // / indentation in list items is lost if #32 characters are used for it.
32+ // / NBSP was chosen since it should render the same as a space if calling
33+ // / code doesn't convert it.</remarks>
34+ LISpacer = NBSP; // Do not localise. Must be <> #32
35+ // / <summary>Bullet character used when rendering unordered list items.
36+ // / </summary>
37+ Bullet = ' *' ; // Do not localise. Must be <> #32 and <> LISpacer
2338 strict private
39+ const
40+ IndentDelta = 2 ;
41+ type
42+ TListKind = (lkNumber, lkBullet);
43+ TListState = record
44+ public
45+ ListNumber: Cardinal;
46+ ListKind: TListKind;
47+ constructor Create(AListKind: TListKind);
48+ end ;
49+ TLIState = record
50+ IsFirstPara: Boolean;
51+ constructor Create(AIsFirstPara: Boolean);
52+ end ;
2453 var
2554 fDisplayURLs: Boolean;
26- fInBlock: Boolean;
2755 fParaBuilder: TStringBuilder;
2856 fDocBuilder: TStringBuilder;
57+ fBlocksStack: TStack<TActiveTextActionElemKind>;
58+ fListStack: TStack<TListState>;
59+ fLIStack: TStack<TLIState>;
60+ fIndent: UInt16;
61+ fInPara: Boolean;
62+ fInListItem: Boolean;
63+ function CanEmitInline : Boolean;
64+ procedure AppendToPara (const AText: string);
2965 procedure InitialiseRender ;
3066 procedure FinaliseRender ;
3167 procedure OutputParagraph ;
3268 procedure RenderTextElem (Elem: IActiveTextTextElem);
3369 procedure RenderBlockActionElem (Elem: IActiveTextActionElem);
3470 procedure RenderInlineActionElem (Elem: IActiveTextActionElem);
3571 procedure RenderURL (Elem: IActiveTextActionElem);
72+ function Render (ActiveText: IActiveText): string;
3673 public
3774 constructor Create;
3875 destructor Destroy; override;
3976 property DisplayURLs: Boolean read fDisplayURLs write fDisplayURLs
4077 default False;
41- function Render (ActiveText: IActiveText): string;
78+ function RenderWrapped (ActiveText: IActiveText; const PageWidth, LMargin,
79+ ParaOffset: Cardinal; const Prefix: string = ' ' ;
80+ const Suffix: string = ' ' ): string;
4281 end ;
4382
4483
4584implementation
4685
4786uses
87+ // Delphi
88+ Character,
89+ // Project
90+ UIStringList,
4891 UStrUtils;
4992
5093{ TActiveTextTextRenderer }
5194
95+ procedure TActiveTextTextRenderer.AppendToPara (const AText: string);
96+ begin
97+ if AText = ' ' then
98+ Exit;
99+ fParaBuilder.Append(AText);
100+ fInPara := True;
101+ end ;
102+
103+ function TActiveTextTextRenderer.CanEmitInline : Boolean;
104+ begin
105+ if fBlocksStack.Count <= 0 then
106+ Exit(False);
107+ Result := TActiveTextElemCaps.CanContainText(fBlocksStack.Peek);
108+ end ;
109+
52110constructor TActiveTextTextRenderer.Create;
53111begin
112+ Assert(LISpacer <> ' ' , ClassName + ' .Create: LISpacer can'' t be #32' );
113+ Assert(Bullet <> ' ' , ClassName + ' .Create: Bullet can'' t be #32' );
114+ Assert(Bullet <> LISpacer, ClassName + ' .Create: Bullet = LISpacer' );
54115 inherited Create;
55116 fParaBuilder := TStringBuilder.Create;
56117 fDocBuilder := TStringBuilder.Create;
57118 fDisplayURLs := False;
119+ fBlocksStack := TStack<TActiveTextActionElemKind>.Create;
120+ fListStack := TStack<TListState>.Create;
121+ fLIStack := TStack<TLIState>.Create;
122+ fIndent := 0 ;
123+ fInPara := False;
124+ fInListItem := False;
58125end ;
59126
60127destructor TActiveTextTextRenderer.Destroy;
61128begin
129+ fLIStack.Free;
130+ fListStack.Free;
131+ fBlocksStack.Free;
62132 fDocBuilder.Free;
63133 fParaBuilder.Free;
64134 inherited ;
@@ -76,11 +146,33 @@ procedure TActiveTextTextRenderer.InitialiseRender;
76146end ;
77147
78148procedure TActiveTextTextRenderer.OutputParagraph ;
149+ var
150+ LIState: TLIState;
79151begin
80152 if fParaBuilder.Length = 0 then
81153 Exit;
82- fDocBuilder.AppendLine(StrTrim(fParaBuilder.ToString));
154+ fDocBuilder.Append(StrOfChar(NBSP, fIndent));
155+ if fInListItem and not fLIStack.Peek.IsFirstPara then
156+ // Do we need fInListItem? - test for non-empty list stack?
157+ // if we do need it, put it on list stack
158+ fDocBuilder.Append(StrOfChar(NBSP, IndentDelta));
159+ if fLIStack.Count > 0 then
160+ begin
161+ if not fLIStack.Peek.IsFirstPara then
162+ begin
163+ fDocBuilder.Append(StrOfChar(NBSP, IndentDelta));
164+ end
165+ else
166+ begin
167+ // Update item at top of stack
168+ LIState := fLIStack.Pop;
169+ LIState.IsFirstPara := False;
170+ fLIStack.Push(LIState);
171+ end ;
172+ end ;
173+ fDocBuilder.AppendLine(StrTrimRight(fParaBuilder.ToString));
83174 fParaBuilder.Clear;
175+ fInPara := False;
84176end ;
85177
86178function TActiveTextTextRenderer.Render (ActiveText: IActiveText): string;
@@ -90,7 +182,6 @@ function TActiveTextTextRenderer.Render(ActiveText: IActiveText): string;
90182 ActionElem: IActiveTextActionElem;
91183begin
92184 InitialiseRender;
93- fInBlock := False;
94185 for Elem in ActiveText do
95186 begin
96187 if Supports(Elem, IActiveTextTextElem, TextElem) then
@@ -109,42 +200,212 @@ function TActiveTextTextRenderer.Render(ActiveText: IActiveText): string;
109200
110201procedure TActiveTextTextRenderer.RenderBlockActionElem (
111202 Elem: IActiveTextActionElem);
203+ var
204+ ListState: TListState;
112205begin
113206 case Elem.State of
114207 fsOpen:
115208 begin
116- fInBlock := True;
209+ fBlocksStack.Push(Elem.Kind);
210+ case Elem.Kind of
211+ ekPara: { Do nothing} ;
212+ ekHeading: { Do nothing} ;
213+ ekUnorderedList:
214+ begin
215+ if (fListStack.Count > 0 ) and (fInPara) then
216+ OutputParagraph;
217+ fListStack.Push(TListState.Create(lkBullet));
218+ Inc(fIndent, IndentDelta);
219+ end ;
220+ ekOrderedList:
221+ begin
222+ if (fListStack.Count > 0 ) and (fInPara) then
223+ OutputParagraph;
224+ fListStack.Push(TListState.Create(lkNumber));
225+ Inc(fIndent, IndentDelta);
226+ end ;
227+ ekListItem:
228+ begin
229+ // Update list number of current list
230+ ListState := fListStack.Pop;
231+ Inc(ListState.ListNumber, 1 );
232+ fListStack.Push(ListState);
233+ // Push this list item to list item stack
234+ fLIStack.Push(TLIState.Create(True));
235+ // Act depending on current list kind
236+ case fListStack.Peek.ListKind of
237+ lkNumber:
238+ begin
239+ // Number list: start a new numbered item, with current number
240+ fParaBuilder.Append(IntToStr(fListStack.Peek.ListNumber));
241+ fParaBuilder.Append(NBSP);
242+ end ;
243+ lkBullet:
244+ begin
245+ // Bullet list: start a new bullet point
246+ fParaBuilder.Append(Bullet + NBSP);
247+ end ;
248+ end ;
249+ end ;
250+ end ;
117251 end ;
118252 fsClose:
119253 begin
120- OutputParagraph;
121- fInBlock := False;
254+ case Elem.Kind of
255+ ekPara:
256+ OutputParagraph;
257+ ekHeading:
258+ OutputParagraph;
259+ ekUnorderedList:
260+ begin
261+ OutputParagraph;
262+ fListStack.Pop;
263+ Dec(fIndent, IndentDelta);
264+ end ;
265+ ekOrderedList:
266+ begin
267+ OutputParagraph;
268+ fListStack.Pop;
269+ Dec(fIndent, IndentDelta);
270+ end ;
271+ ekListItem:
272+ begin
273+ OutputParagraph;
274+ fInListItem := False;
275+ fLIStack.Pop;
276+ end ;
277+ end ;
278+ fBlocksStack.Pop;
122279 end ;
123280 end ;
124281end ;
125282
126283procedure TActiveTextTextRenderer.RenderInlineActionElem (
127284 Elem: IActiveTextActionElem);
128285begin
129- if not fInBlock then
286+ if not CanEmitInline then
130287 Exit;
131288 if (Elem.Kind = ekLink) and (Elem.State = fsClose) and fDisplayURLs then
132289 RenderURL(Elem);
290+ // else ignore element: formatting elements have no effect on plain text
133291end ;
134292
135293procedure TActiveTextTextRenderer.RenderTextElem (Elem: IActiveTextTextElem);
294+ var
295+ TheText: string;
136296begin
137- if not fInBlock then
297+ if not CanEmitInline then
138298 Exit;
139- fParaBuilder.Append(Elem.Text);
299+ TheText := Elem.Text;
300+ // no white space emitted after block start until 1st non-white space
301+ // character encountered
302+ if not fInPara then
303+ TheText := StrTrimLeft(Elem.Text);
304+ if TheText = ' ' then
305+ Exit;
306+ AppendToPara(TheText);
140307end ;
141308
142309procedure TActiveTextTextRenderer.RenderURL (Elem: IActiveTextActionElem);
143310resourcestring
144311 sURL = ' (%s)' ; // formatting for URLs from hyperlinks
145312begin
146313 Assert(Elem.Kind = ekLink, ClassName + ' .RenderURL: Not a link element' );
147- fParaBuilder.AppendFormat(sURL, [Elem.Attrs[TActiveTextAttrNames.Link_URL]]);
314+ AppendToPara(Format(sURL, [Elem.Attrs[TActiveTextAttrNames.Link_URL]]));
315+ end ;
316+
317+ function TActiveTextTextRenderer.RenderWrapped (ActiveText: IActiveText;
318+ const PageWidth, LMargin, ParaOffset: Cardinal; const Prefix, Suffix: string):
319+ string;
320+ var
321+ Paras: IStringList;
322+ Para: string;
323+ ParaIndent: UInt16;
324+ WrappedPara: string;
325+ Offset: Int16;
326+
327+ // Calculate indent of paragraph by counting LISpacer characters inserted by
328+ // Render method
329+ function CalcParaIndent : UInt16;
330+ var
331+ Ch: Char;
332+ begin
333+ Result := 0 ;
334+ for Ch in Para do
335+ begin
336+ if Ch <> LISpacer then
337+ Break;
338+ Inc(Result);
339+ end ;
340+ end ;
341+
342+ // Calculate if we are currently processing a list item by detecting Bullet,
343+ // digits and LISpacer characters inserted by Render method
344+ function IsListItem : Boolean;
345+ var
346+ Remainder: string;
347+ Digits: string;
348+ Ch: Char;
349+ begin
350+ Result := False;
351+ // Strip any leading spacer chars from start of para
352+ Remainder := StrTrimLeftChars(Para, LISpacer);
353+ // Check for bullet list: starts with bullet character then spacer
354+ if StrStartsStr(Bullet + LISpacer, Remainder) then
355+ Exit(True);
356+ // Check for number list: starts with digit(s) then spacer
357+ Digits := ' ' ;
358+ for Ch in Remainder do
359+ if TCharacter.IsDigit(Ch) then
360+ Digits := Digits + Ch
361+ else
362+ Break;
363+ if (Digits <> ' ' ) and
364+ StrStartsStr(Digits + LISpacer, Remainder) then
365+ Exit(True);
366+ end ;
367+
368+ begin
369+ Result := ' ' ;
370+ Paras := TIStringList.Create(Prefix + Render(ActiveText) + Suffix, EOL, True);
371+ for Para in Paras do
372+ begin
373+ if IsListItem then
374+ begin
375+ Offset := -ParaOffset;
376+ ParaIndent := CalcParaIndent + LMargin + ParaOffset;
377+ end
378+ else
379+ begin
380+ Offset := 0 ;
381+ ParaIndent := CalcParaIndent + LMargin;
382+ end ;
383+ WrappedPara := StrWrap(
384+ StrReplace(Para, LISpacer, ' ' ),
385+ PageWidth - ParaIndent,
386+ ParaIndent,
387+ Offset
388+ );
389+ if Result <> ' ' then
390+ Result := Result + EOL;
391+ Result := Result + StrTrimRight(WrappedPara);
392+ end ;
393+ Result := StrTrimRight(Result);
394+ end ;
395+
396+ { TActiveTextTextRenderer.TListState }
397+
398+ constructor TActiveTextTextRenderer.TListState.Create(AListKind: TListKind);
399+ begin
400+ ListNumber := 0 ;
401+ ListKind := AListKind;
402+ end ;
403+
404+ { TActiveTextTextRenderer.TLIState }
405+
406+ constructor TActiveTextTextRenderer.TLIState.Create(AIsFirstPara: Boolean);
407+ begin
408+ IsFirstPara := AIsFirstPara;
148409end ;
149410
150411end .
0 commit comments