{
* 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 http://mozilla.org/MPL/2.0/
*
* Copyright (C) 2008-2013, Peter Johnson (www.delphidabbler.com).
*
* $Rev$
* $Date$
*
* Provides interfaces, a factory class and implementation of "active text".
* Active text is text that can have actions performed on it. Actions may
* include formatting and clickable links.
*
* The active text object provides a list of a mixture of text and compound
* action elements. Text elements contain text to be displayed in the current
* context. Action elements occur in matched pairs and specify an action to be
* performed: the first action element switches on the action and the second
* switches it off. Action elements can either change the display context or
* define some action to be performed, or both.
*
* Active text does not define how it is rendered. It is up to the user to
* determine how to render the text by examining the elements. Some elements are
* defined as block level and some as inline, which provides a hint as to how to
* render.
*
* An active text object can be created by interpreting a textual markup
* language. The object is language agnostic. The user must provide a parser
* that can interpret the language and create the required active text elements.
}
unit CS.ActiveText;
interface
uses
// Delphi
Generics.Collections,
// Project
UBaseObjects,
UEncodings,
UExceptions;
type
/// <summary>Contains constants that name the attributes supported by
/// different kinds of active text element.</summary>
TActiveTextAttrNames = record
public
const
/// <summary>Name of attribute that stores a link tag's URI.</summary>
Link_URL = 'href';
end;
/// <summary>Indicates whether a active text action element is opening or
/// closing.</summary>
/// <remarks>An opening action element changes the state of the document
/// while a closing element restores the document state to that pertaining
/// before the opening element was encountered.</remarks>
TActiveTextElemState = (
fsClose, // element is closing
fsOpen // element is opening
);
/// <summary>Indicates how an active text action element is displayed.
/// </summary>
TActiveTextDisplayStyle = (
dsInline, // element is inline
dsBlock // element is block
);
/// <summary>Base, do nothing, interface supported by all active text
/// elements.</summary>
IActiveTextElem = interface(IInterface)
['{F08A9853-EDB6-4B14-8E21-F3AB10FAF7D9}']
end;
/// <summary>Interface supported by plain text active text elements.
/// </summary>
/// <remarks>All instances that support this interface kind Kind = ekText.
/// </remarks>
IActiveTextTextElem = interface(IActiveTextElem)
['{B20C56D2-4ACC-48C8-AB30-9979A1B148B3}']
/// <summary>Returns plain text represented by element.</summary>
function GetText: string;
/// <summary>Plain text represented by element.</summary>
property Text: string read GetText;
end;
/// <summary>Encapsulates an attribute of a active text action element.
/// </summary>
/// <remarks>Key field is attribute name and Value field is attribute value.
/// </remarks>
TActiveTextAttr = TPair<string,string>;
/// <summary>Interface supported by active text attributes.</summary>
IActiveTextAttrs = interface(IInterface)
['{6AE73940-60C4-4097-8DB8-6A88C97D7DA7}']
/// <summary>Returns value of named attribute.</summary>
/// <remarks>Exception raised if no such attribute exists.</remarks>
function GetAttr(const Name: string): string;
/// <summary>Array property indexed by attribute name and returning
/// attribute values.</summary>
/// <remarks>Exception raised if no such attribute exists.</remarks>
property Attrs[const Name: string]: string read GetAttr; default;
/// <summary>Returns number of attributes.</summary>
function Count: Integer;
/// <summary>Records the value of a named attribute.</summary>
procedure Add(const Name, Value: string);
/// <summary>Returns a new attribute enumerator.</summary>
/// <remarks>Caller is responsible for freeing the enumerator.</remarks>
function GetEnumerator: TEnumerator<TActiveTextAttr>;
end;
/// <summary>Supported types of active text action elements.</summary>
TActiveTextActionElemKind = (
ekLink, // link element: has a URL (inline)
ekStrong, // text formatted as strong (inline)
ekEm, // text formatted as emphasised (inline)
ekVar, // text formatted as variable (inline)
ekPara, // delimits a paragraph (block level)
ekWarning, // text formatted as a warning (inline)
ekHeading, // delimits a heading (block level)
ekMono // text formatted as mono spaced (inline)
);
/// <summary>Set of types of active text action elements.</summary>
TActiveTextActionElemKinds = set of TActiveTextActionElemKind;
/// <summary>Interface supported by active text action elements, i.e. those
/// that specify actions to be performed on text.</summary>
/// <remarks>Actions include formatting text and hot links.</remarks>
IActiveTextActionElem = interface(IActiveTextElem)
['{2956A28F-AED2-437E-A405-9A62077BD881}']
/// <summary>Returns kind of action represented by this element.</summary>
function GetKind: TActiveTextActionElemKind;
/// <summary>Kind of action represented by this element.</summary>
property Kind: TActiveTextActionElemKind read GetKind;
/// <summary>Gets state of element. Informs whether this element is an
/// opening or closing element.</summary>
/// <remarks>Opening elements switch on the action and closing elements
/// switch off the action.</remarks>
function GetState: TActiveTextElemState;
/// <summary>Indicates whether element is opening or closing operation.
/// </summary>
/// <remarks>Operation is determined by inherited Kind property.</remarks>
property State: TActiveTextElemState read GetState;
/// <summary>Returns object describing element's attributes.</summary>
function GetAttrs: IActiveTextAttrs;
/// <summary>Object describing element's attributes.</summary>
property Attrs: IActiveTextAttrs read GetAttrs;
/// <summary>Returns value that indicates whether element is an inline or
/// block element.</summary>
function GetDisplayStyle: TActiveTextDisplayStyle;
/// <summary>Indicates whether element displays inline or as a block.
/// </summary>
property DisplayStyle: TActiveTextDisplayStyle read GetDisplayStyle;
end;
/// <summary>Interface supported by objects that can render active text into
/// some other form.</summary>
IActiveTextRenderer = interface(IInterface)
['{88C9312F-1485-4C55-B1F7-A4A9FC13E00E}']
/// <summary>Called before active text is processed.</summary>
procedure Initialise;
/// <summary>Called after last active text element has been processed.
/// </summary>
procedure Finalise;
/// <summary>Called when plain text should be output.</summary>
/// <param name="AText">string. Text to be output.</param>
procedure OutputText(const AText: string);
/// <summary>Called at the start of a new block of active text.</summary>
/// <param name="Kind">TActiveTextActionElemKind [in] Kind of block being
/// opened. This will be an active text element with DisplayStyle =
/// dsBlock.</param>
procedure BeginBlock(const Kind: TActiveTextActionElemKind);
/// <summary>Called when the current active text block ends.</summary>
/// <param name="Kind">TActiveTextActionElemKind [in] Kind of block being
/// opened. This will be an active text element with DisplayStyle =
/// dsBlock.</param>
procedure EndBlock(const Kind: TActiveTextActionElemKind);
/// <summary>Called at the start of a new active text styling element.
/// </summary>
/// <param name="Kind">TActiveTextActionElemKind [in] Kind of styling
/// element. The kind indicates the type of styling to be applied. This
/// will be an active text element with DisplayStyle = dsInline that is
/// not ekLink.</param>
procedure BeginInlineStyle(const Kind: TActiveTextActionElemKind);
/// <summary>Called the current active text styling element ends.</summary>
/// <param name="Kind">TActiveTextActionElemKind [in] Kind of styling
/// element. The kind indicates the type of styling to be applied. This
/// will be an active text element with DisplayStyle = dsInline that is
/// not ekLink.</param>
procedure EndInlineStyle(const Kind: TActiveTextActionElemKind);
/// <summary>Called at the start of a new active text link element.
/// </summary>
/// <param name="URL">string. URL to accessed from the link.</param>
procedure BeginLink(const URL: string);
/// <summary>Called when the current active text link element ends.
/// </summary>
/// <param name="URL">string. URL to accessed from the link.</param>
procedure EndLink(const URL: string);
end;
/// <summary>Interface that defines operations of active text objects.
/// </summary>
IActiveText = interface(IInterface)
['{230228FB-355F-4EC9-9EA9-F8A6DE628972}']
/// <summary>Gets enumerator for active text object's elements.</summary>
function GetEnumerator: TEnumerator<IActiveTextElem>;
/// <summary>Adds given element to active text object and returns index of
/// element in list.</summary>
function AddElem(const Elem: IActiveTextElem): Integer;
/// <summary>Appends elements from another given active text object to the
/// current object.</summary>
procedure Append(const ActiveText: IActiveText);
/// <summary>Checks if the active text object contains any elements.
/// </summary>
function IsEmpty: Boolean;
/// <summary>Checks if the active text object contains only plain text.
/// </summary>
/// <remarks>Plain text is considered to be active text with no action
/// elements except for "para". This can rendered in plain text with no
/// loss of formatting.</remarks>
function IsPlainText: Boolean;
/// <summary>Returns element at given index in active text object's element
/// list.</summary>
function GetElem(Idx: Integer): IActiveTextElem;
/// <summary>Returns number of elements in active text object's element
/// list.</summary>
function GetCount: Integer;
/// <summary>Renders active text object using given renderer object.
/// </summary>
/// <remarks>How the rendered data is used or out is determined by the
/// renderer.</remarks>
procedure Render(Renderer: IActiveTextRenderer);
/// <summary>Returns a copy of the active text object converted into normal
/// form.</summary>
/// <remarks>Any REML not contained with a block is enclosed within a
/// specially created paragraph block.</remarks>
function Normalise: IActiveText;
/// <summary>List of elements in active text object.</summary>
property Elems[Idx: Integer]: IActiveTextElem read GetElem; default;
/// <summary>Number of elements in element list.</summary>
property Count: Integer read GetCount;
end;
/// <summary>Interface supported by objects that can build an active text
/// object by parsing mark-up.</summary>
/// <remarks>Markup format is not specified: different implementations of
/// this interface may support different markup.</remarks>
IActiveTextParser = interface(IInterface)
['{C17BC6AA-6012-4530-A39D-9807C1A1AF42}']
/// <summary>Parses markup into active text.</summary>
/// <param name="Markup">string [in] Markup that describes active text.
/// </param>
/// <returns>IActiveText. Active text object created by parser.</returns>
function Parse(const Markup: string): IActiveText;
end;
/// <summary>Class of exception raised when parsing active text markup.
/// </summary>
EActiveTextParserError = class(EValidation);
/// <summary>Static factory class that can create instances of active text
/// objects and active text elements.</summary>
TActiveTextFactory = class(TNoConstructObject)
public
/// <summary>Returns a cloned copy of a given active text object.</summary>
class function CloneActiveText(const Src: IActiveText): IActiveText;
/// <summary>Returns a new empty active text object.</summary>
class function CreateActiveText: IActiveText; overload;
/// <summary>Returns an active text object with contents obtained by
/// parsing some markup.</summary>
/// <param name="Markup">string [in] Markup to be parsed.</param>
/// <param name="Parser">IActiveTextParser [in] Parser to be used to
/// parse markup.</param>
/// <returns>IActiveText. Active text object created from markup.</returns>
class function CreateActiveText(const Markup: string;
Parser: IActiveTextParser): IActiveText; overload;
/// <summary>Returns a new active text text element for given text.
/// </summary>
class function CreateTextElem(const AText: string): IActiveTextTextElem;
/// <summary>Returns a new active text action element.</summary>
/// <param name="Kind">TActiveTextElemKind [in] Kind of element to be
/// created.</param>
/// <param name="Attrs">IActiveTextAttrs [in] Attributes of new element.
/// </param>
/// <param name="State">TActiveTextElemState [in] State of element (opening
/// or closing).</param>
/// <returns>IActiveTextActionElem. New element.</returns>
class function CreateActionElem(const Kind: TActiveTextActionElemKind;
Attrs: IActiveTextAttrs; const State: TActiveTextElemState):
IActiveTextActionElem; overload;
/// <summary>Returns a new active text action element with no attributes.
/// </summary>
/// <param name="Kind">TActiveTextElemKind [in] Kind of element to be
/// created.</param>
/// <param name="State">TActiveTextElemState [in] State of element (opening
/// or closing).</param>
/// <returns>IActiveTextActionElem. New element.</returns>
class function CreateActionElem(const Kind: TActiveTextActionElemKind;
const State: TActiveTextElemState): IActiveTextActionElem; overload;
/// <summary>Returns a new empty active text attributes object.</summary>
class function CreateAttrs: IActiveTextAttrs; overload;
/// <summary>Returns a new active text attributes object with the single
/// given attribute.</summary>
class function CreateAttrs(Attr: TActiveTextAttr): IActiveTextAttrs;
overload;
/// <summary>Returns a new active text attributes object containing all the
/// attributes from the given array.</summary>
class function CreateAttrs(const Attrs: array of TActiveTextAttr):
IActiveTextAttrs; overload;
end;
implementation
uses
// Delphi
SysUtils,
// Project
IntfCommon,
UStrUtils;
type
/// <summary>Active text object implementation.</summary>
TActiveText = class(TInterfacedObject,
IActiveText, IAssignable, IClonable
)
strict private
/// <summary> List of active text elements.</summary>
fElems: TList<IActiveTextElem>;
public
/// <summary>Object constructor. Sets up object.</summary>
constructor Create;
/// <summary>Object destructor. Tears down object.</summary>
destructor Destroy; override;
/// <summary>Assigns properties of another object to this object.</summary>
/// <param name="Src">IInterface [in] Object whose properties are to be
/// assigned. Src must support IActiveText but may be nil.</param>
/// <remarks>
/// <para>Method of IAssignable.</para>
/// <para>If Src is nil, all this object's properties are cleared.</para>
/// <para>Raises EBug if Src is not nil and does not support IActiveText.
/// </para>
/// </remarks>
procedure Assign(const Src: IInterface);
/// <summary>Returns a cloned instance of this object.</summary>
/// <remarks>Method of IClonable.</remarks>
function Clone: IInterface;
/// <summary>Gets enumerator for contents of element list.</summary>
/// <remarks>Method of IActiveText.</remarks>
function GetEnumerator: TEnumerator<IActiveTextElem>;
/// <summary>Adds given element to element list and returns index of
/// element in list.</summary>
/// <remarks>Method of IActiveText.</remarks>
function AddElem(const Elem: IActiveTextElem): Integer;
/// <summary>Appends elements from given active text object to this one.
/// </summary>
/// <remarks>Method of IActiveText.</remarks>
procedure Append(const ActiveText: IActiveText);
/// <summary>Checks if the element list is empty.</summary>
/// <remarks>Method of IActiveText.</remarks>
function IsEmpty: Boolean;
/// <summary>Checks if the active text object contains only plain text.
/// </summary>
/// <remarks>
/// <para>Plain text is considered to be active text with no action
/// elements except for "para". This can rendered in plain text with no
/// loss of formatting.</para>
/// <para>Method of IActiveText.</para>
/// </remarks>
function IsPlainText: Boolean;
/// <summary>Returns element at given index in element list.</summary>
/// <remarks>Method of IActiveText.</remarks>
function GetElem(Idx: Integer): IActiveTextElem;
/// <summary>Returns number of elements element list.</summary>
/// <remarks>Method of IActiveText.</remarks>
function GetCount: Integer;
/// <summary>Renders active text object using given renderer object.
/// </summary>
/// <remarks>
/// <para>How the rendered data is used or out is determined by the
/// renderer.</para>
/// <para>Method of IActiveText.</para>
/// </remarks>
procedure Render(Renderer: IActiveTextRenderer);
/// <summary>Returns a copy of the active text object converted into normal
/// form.</summary>
/// <remarks>
/// <para>Any REML not contained with a block is enclosed within a
/// specially created paragraph block.</para>
/// </remarks>
function Normalise: IActiveText;
end;
type
/// <summary>Implements an active text plain text element.</summary>
TActiveTextTextElem = class(TInterfacedObject,
IActiveTextElem, IActiveTextTextElem, IAssignable, IClonable
)
strict private
/// <summary>Element's text.</summary>
fText: string;
public
/// <summary>Object constructor. Records given element text and sets
/// required kind for a text element.</summary>
constructor Create(const Text: string);
/// <summary>Assigns properties of another object to this object.</summary>
/// <param name="Src">IInterface [in] Object whose properties are to be
/// assigned. Src must support IActiveTextTextElem.</param>
/// <remarks>Method of IAssignable.</remarks>
procedure Assign(const Src: IInterface);
/// <summary>Returns a cloned instance of this object.</summary>
/// <remarks>Method of IClonable.</remarks>
function Clone: IInterface;
/// <summary>Returns plain text represented by element.</summary>
/// <remarks>Method of IActiveTextTextElem.</remarks>
function GetText: string;
end;
type
/// <summary>Implements an active text action element.</summary>
TActiveTextActionElem = class(TInterfacedObject,
IActiveTextElem, IActiveTextActionElem, IAssignable, IClonable
)
strict private
/// <summary>Kind of element encapsulated by this object.</summary>
fKind: TActiveTextActionElemKind;
/// <summary>State of element: opening or closing.</summary>
fState: TActiveTextElemState;
/// <summary>Attributes associated with element.</summary>
fAttrs: IActiveTextAttrs;
public
/// <summary>Object constructor. Creates an action element.</summary>
/// <param name="Kind">TActiveTextElemKind [in] Required kind of element.
/// </param>
/// <param name="Attrs">IActiveTextAttrs [in] Element's attributes.</param>
/// <param name="State">TActiveTextElemState [in] State of element: opening
/// or closing.</param>
constructor Create(const Kind: TActiveTextActionElemKind;
Attrs: IActiveTextAttrs; const State: TActiveTextElemState);
/// <summary>Assigns properties of another object to this object.</summary>
/// <param name="Src">IInterface [in] Object whose properties are to be
/// assigned. Src must support IActiveTextActionElem.</param>
/// <remarks>
/// <para>Method of IAssignable.</para>
/// <para>Raises EBug if Src does not support IActiveTextActionElem.</para>
/// </remarks>
procedure Assign(const Src: IInterface);
/// <summary>Returns a cloned instance of this object.</summary>
/// <remarks>Method of IClonable.</remarks>
function Clone: IInterface;
/// <summary>Returns kind of action represented by this element.</summary>
/// <remarks>Method of IActiveTextActionElem.</remarks>
function GetKind: TActiveTextActionElemKind;
/// <summary>Returns state of element.</summary>
/// <remarks>Method of IActiveTextActionElem.</remarks>
function GetState: TActiveTextElemState;
/// <summary>Returns object describing element's attributes.</summary>
/// <remarks>Method of IActiveTextActionElem.</remarks>
function GetAttrs: IActiveTextAttrs;
/// <summary>Returns value that indicates whether element is an inline or
/// block element.</summary>
/// <remarks>Method of IActiveTextActionElem.</remarks>
function GetDisplayStyle: TActiveTextDisplayStyle;
end;
type
/// <summary>Implements an object that encapsulates a set of active text
/// attributes.</summary>
TActiveTextAttrs = class(TInterfacedObject,
IActiveTextAttrs, IAssignable, IClonable
)
strict private
/// <summary>Map of attribute names to values.</summary>
fMap: TDictionary<string,string>;
public
/// <summary>Object constructor. Sets up empty attributes object.</summary>
constructor Create;
/// <summary>Object destructor. Tears down object.</summary>
destructor Destroy; override;
/// <summary>Returns value of named attribute.</summary>
/// <remarks>
/// <para>Method of IActiveTextAttrs.</para>
/// <para>Exception raised if no such attribute exists.</para>
/// </remarks>
function GetAttr(const Name: string): string;
/// <summary>Returns number of attributes.</summary>
/// <remarks>Method of IActiveTextAttrs.</remarks>
function Count: Integer;
/// <summary>Records the value of a named attribute.</summary>
/// <remarks>
/// <para>Method of IActiveTextAttrs.</para>
/// <para>Any existing value with same name is overwritten.</para>
/// </remarks>
procedure Add(const Name, Value: string);
/// <summary>Returns a new attribute enumerator.</summary>
/// <remarks>
/// <para>Method of IActiveTextAttrs.</para>
/// <para>Caller is responsible for freeing the enumerator.</para>
/// </remarks>
function GetEnumerator: TEnumerator<TActiveTextAttr>;
/// <summary>Assigns properties of another object to this object.</summary>
/// <param name="Src">IInterface [in] Object whose properties are to be
/// assigned. Src must support IActiveTextAttrs.</param>
/// <remarks>Method of IAssignable.</remarks>
procedure Assign(const Src: IInterface);
/// <summary>Returns a cloned instance of this object.</summary>
/// <remarks>Method of IClonable.</remarks>
function Clone: IInterface;
end;
{ TActiveTextFactory }
class function TActiveTextFactory.CloneActiveText(
const Src: IActiveText): IActiveText;
begin
Result := CreateActiveText;
(Result as IAssignable).Assign(Src);
end;
class function TActiveTextFactory.CreateActionElem(
const Kind: TActiveTextActionElemKind; Attrs: IActiveTextAttrs;
const State: TActiveTextElemState): IActiveTextActionElem;
begin
Result := TActiveTextActionElem.Create(Kind, Attrs, State);
end;
class function TActiveTextFactory.CreateActionElem(
const Kind: TActiveTextActionElemKind;
const State: TActiveTextElemState): IActiveTextActionElem;
begin
Result := CreateActionElem(Kind, TActiveTextAttrs.Create, State);
end;
class function TActiveTextFactory.CreateActiveText: IActiveText;
begin
Result := TActiveText.Create;
end;
class function TActiveTextFactory.CreateActiveText(const Markup: string;
Parser: IActiveTextParser): IActiveText;
begin
Result := Parser.Parse(Markup);
end;
class function TActiveTextFactory.CreateAttrs(
const Attrs: array of TActiveTextAttr): IActiveTextAttrs;
var
Attr: TActiveTextAttr;
begin
Result := TActiveTextAttrs.Create;
for Attr in Attrs do
Result.Add(Attr.Key, Attr.Value);
end;
class function TActiveTextFactory.CreateAttrs(Attr: TActiveTextAttr):
IActiveTextAttrs;
begin
Result := CreateAttrs([Attr]);
end;
class function TActiveTextFactory.CreateAttrs: IActiveTextAttrs;
begin
Result := TActiveTextAttrs.Create;
end;
class function TActiveTextFactory.CreateTextElem(
const AText: string): IActiveTextTextElem;
begin
Result := TActiveTextTextElem.Create(AText);
end;
{ TActiveText }
function TActiveText.AddElem(const Elem: IActiveTextElem): Integer;
begin
Result := fElems.Add(Elem);
end;
procedure TActiveText.Append(const ActiveText: IActiveText);
var
Elem: IActiveTextElem; // references each element in elems
NewElem: IActiveTextElem;
begin
for Elem in ActiveText do
begin
NewElem := (Elem as IClonable).Clone as IActiveTextElem;
AddElem(NewElem);
end;
end;
procedure TActiveText.Assign(const Src: IInterface);
begin
if Assigned(Src) and not Supports(Src, IActiveText) then
raise EBug.Create(ClassName + '.Assign: Src must support IActiveText');
fElems.Clear;
if Assigned(Src) then
Append(Src as IActiveText);
end;
function TActiveText.Clone: IInterface;
begin
Result := Create;
(Result as IAssignable).Assign(Self);
end;
constructor TActiveText.Create;
begin
inherited Create;
fElems := TList<IActiveTextElem>.Create;
end;
destructor TActiveText.Destroy;
begin
fElems.Free;
inherited;
end;
function TActiveText.GetCount: Integer;
begin
Result := fElems.Count;
end;
function TActiveText.GetElem(Idx: Integer): IActiveTextElem;
begin
Result := fElems[Idx];
end;
function TActiveText.GetEnumerator: TEnumerator<IActiveTextElem>;
begin
Result := fElems.GetEnumerator;
end;
function TActiveText.IsEmpty: Boolean;
begin
Result := fElems.Count = 0;
end;
function TActiveText.IsPlainText: Boolean;
var
Elem: IActiveTextElem;
ActionElem: IActiveTextActionElem;
begin
for Elem in fElems do
begin
if Supports(Elem, IActiveTextActionElem, ActionElem)
and (ActionElem.Kind <> ekPara) then
Exit(False);
end;
Result := True;
end;
function TActiveText.Normalise: IActiveText;
// Checks if the given active text element is an opening block tag.
function IsBlockOpener(Elem: IActiveTextElem): Boolean;
var
ActionElem: IActiveTextActionElem;
begin
if not Supports(Elem, IActiveTextActionElem, ActionElem) then
Exit(False);
Result := (ActionElem.DisplayStyle = dsBlock)
and (ActionElem.State = fsOpen);
end;
// Checks if the given active text element is a closing block tag.
function IsBlockCloser(Elem: IActiveTextElem): Boolean;
var
ActionElem: IActiveTextActionElem;
begin
if not Supports(Elem, IActiveTextActionElem, ActionElem) then
Exit(False);
Result := (ActionElem.DisplayStyle = dsBlock)
and (ActionElem.State = fsClose);
end;
type
/// <summary>Describes different parts of parsed REML code in relation to
/// block tags.</summary>
/// <remarks>Can be within a pair of block tags; without, i.e. not enclosed
/// by block tags or in the transitional state between one and the other.
/// </remarks>
TBlockState = (bsWithin, bsWithout, bsTransition);
var
TextElem: IActiveTextTextElem;
Text: string;
BlockState: TBlockState;
Elem: IActiveTextElem;
begin
Result := TActiveTextFactory.CreateActiveText;
if IsEmpty then
Exit;
// Scan active text, inserting paragraph level block tags where the active
// text is not enclosed by them: this can be at the start, at the end or
// between existing blocks. E.g for "xxx <p>yyy</p> xxx <p>yyy</p> xxx", xxx
// is without any block and will be enclosed in paragraphs while yyy is within
// a block and will be unchanged.
BlockState := bsTransition;
for Elem in fElems do
begin
if IsBlockOpener(Elem) then
begin
Assert(BlockState <> bsWithin,
ClassName + '.Normalise: Block is nested.');
if BlockState = bsWithout then
Result.AddElem(TActiveTextFactory.CreateActionElem(ekPara, fsClose));
Result.AddElem(Elem);
BlockState := bsWithin;
end
else if IsBlockCloser(Elem) then
begin
Assert(BlockState = bsWithin,
ClassName + '.Normalise: Block closer outside block.');
Result.AddElem(Elem);
BlockState := bsTransition;
end
else
begin
if BlockState = bsTransition then
begin
if Supports(Elem, IActiveTextTextElem, TextElem) then
begin
// make sure we don't start a paragraph block if text contains only
// spaces
Text := StrTrimLeft(TextElem.Text);
if Text <> '' then
begin
Result.AddElem(TActiveTextFactory.CreateActionElem(ekPara, fsOpen));
Result.AddElem(TActiveTextFactory.CreateTextElem(Text));
BlockState := bsWithout;
end;
end
else
begin
Result.AddElem(TActiveTextFactory.CreateActionElem(ekPara, fsOpen));
Result.AddElem(Elem);
BlockState := bsWithout;
end;
end
else
Result.AddElem(Elem);
end;
end;
if BlockState = bsWithout then
Result.AddElem(TActiveTextFactory.CreateActionElem(ekPara, fsClose));
end;
procedure TActiveText.Render(Renderer: IActiveTextRenderer);
var
Elem: IActiveTextElem;
TextElem: IActiveTextTextElem;
ActionElem: IActiveTextActionElem;
begin
Renderer.Initialise;
for Elem in fElems do
begin
if Supports(Elem, IActiveTextTextElem, TextElem) then
Renderer.OutputText(TextElem.Text)
else if Supports(Elem, IActiveTextActionElem, ActionElem) then
begin
if ActionElem.DisplayStyle = dsBlock then
begin
if ActionElem.State = fsOpen then
Renderer.BeginBlock(ActionElem.Kind)
else
Renderer.EndBlock(ActionElem.Kind)
end
else
begin
Assert(ActionElem.DisplayStyle = dsInline,
ClassName + '.Render: Unknown display style for IActiveTextElem');
if ActionElem.Kind = ekLink then
begin
if ActionElem.State = fsOpen then
Renderer.BeginLink(ActionElem.Attrs[TActiveTextAttrNames.Link_URL])
else
Renderer.EndLink(ActionElem.Attrs[TActiveTextAttrNames.Link_URL]);
end
else
begin
if ActionElem.State = fsOpen then
Renderer.BeginInlineStyle(ActionElem.Kind)
else
Renderer.EndInlineStyle(ActionElem.Kind);
end
end;
end;
end;
Renderer.Finalise;
end;
{ TActiveTextTextElem }
procedure TActiveTextTextElem.Assign(const Src: IInterface);
begin
fText := (Src as IActiveTextTextElem).Text;
end;
function TActiveTextTextElem.Clone: IInterface;
begin
Result := TActiveTextTextElem.Create(fText);
(Result as IAssignable).Assign(Self);
end;
constructor TActiveTextTextElem.Create(const Text: string);
begin
inherited Create;
fText := Text;
end;
function TActiveTextTextElem.GetText: string;
begin
Result := fText;
end;
{ TActiveTextActionElem }
procedure TActiveTextActionElem.Assign(const Src: IInterface);
var
SrcElem: IActiveTextActionElem;
begin
if not Supports(Src, IActiveTextActionElem, SrcElem) then
raise EBug.Create(ClassName + '.Assign: Src is not IActiveTextActionElem');
fKind := SrcElem.Kind;
fState := SrcElem.State;
(SrcElem.Attrs as IAssignable).Assign(SrcElem.Attrs);
end;
function TActiveTextActionElem.Clone: IInterface;
var
Attrs: IActiveTextAttrs;
begin
Attrs := (fAttrs as IClonable).Clone as IActiveTextAttrs;
Result := TActiveTextActionElem.Create(GetKind, Attrs, GetState);
end;
constructor TActiveTextActionElem.Create(const Kind: TActiveTextActionElemKind;
Attrs: IActiveTextAttrs; const State: TActiveTextElemState);
begin
inherited Create;
fAttrs := Attrs;
fState := State;
fKind := Kind;
end;
function TActiveTextActionElem.GetAttrs: IActiveTextAttrs;
begin
Result := fAttrs;
end;
function TActiveTextActionElem.GetDisplayStyle: TActiveTextDisplayStyle;
begin
if GetKind in [ekPara, ekHeading] then
Result := dsBlock
else
Result := dsInline;
end;
function TActiveTextActionElem.GetKind: TActiveTextActionElemKind;
begin
Result := fKind;
end;
function TActiveTextActionElem.GetState: TActiveTextElemState;
begin
Result := fState;
end;
{ TActiveTextAttrs }
procedure TActiveTextAttrs.Add(const Name, Value: string);
begin
fMap.Add(Name, Value);
end;
procedure TActiveTextAttrs.Assign(const Src: IInterface);
var
Attr: TActiveTextAttr;
begin
fMap.Clear;
for Attr in (Src as IActiveTextAttrs) do
fMap.Add(Attr.Key, Attr.Value);
end;
function TActiveTextAttrs.Clone: IInterface;
begin
Result := TActiveTextAttrs.Create;
(Result as IAssignable).Assign(Self);
end;
function TActiveTextAttrs.Count: Integer;
begin
Result := fMap.Count;
end;
constructor TActiveTextAttrs.Create;
begin
inherited Create;
fMap := TDictionary<string,string>.Create;
end;
destructor TActiveTextAttrs.Destroy;
begin
fMap.Free;
inherited;
end;
function TActiveTextAttrs.GetAttr(const Name: string): string;
begin
Result := fMap[Name];
end;
function TActiveTextAttrs.GetEnumerator: TEnumerator<TPair<string, string>>;
begin
Result := fMap.GetEnumerator;
end;
end.