source: branches/pre_reorg/Components/RichTextDocumentUnit.pas

Last change on this file was 405, checked in by RBRi, 9 years ago

code format

  • Property svn:eol-style set to native
File size: 18.6 KB
Line 
1Unit RichTextDocumentUnit;
2
3// Declarations of tags, and parsing functions
4
5Interface
6
7uses
8 Classes;
9
10type
11 TTagType = ( ttInvalid,
12 ttBold, ttBoldOff,
13 ttItalic, ttItalicOff,
14 ttUnderline, ttUnderlineOff,
15 ttFixedWidthOn, ttFixedWidthOff,
16 ttHeading1, ttHeading2, ttHeading3, ttHeadingOff,
17 ttColor, ttColorOff,
18 ttBackgroundColor, ttBackgroundColorOff,
19 ttRed, ttBlue, ttGreen, ttBlack,
20 ttWrap,
21 ttAlign,
22 ttBeginLink, ttEndLink,
23 ttSetLeftMargin, ttSetRightMargin,
24 ttImage,
25 ttFont, ttFontOff,
26 ttEnd );
27
28 TStandardColor = record
29 Name: string[ 32 ];
30 Color: TColor;
31 end;
32
33 TTag = record
34 TagType: TTagType;
35 Arguments: string;
36 end;
37
38 TTextElementType = ( teText, // a character
39 teWordBreak,
40 teLineBreak, // end of para
41 teTextEnd,
42 teImage,
43 teStyle );
44
45 TTextElement = record
46 ElementType: TTextElementType;
47 Character: Char;
48 Tag: TTag;
49 end;
50
51 TTextAlignment = ( taLeft,
52 taRight,
53 taCenter );
54
55// Returns tag pointed to by TextPointer and
56// moves TextPointer to the first char after the tag.
57Function ExtractTag( Var TextPointer: PChar ): TTag;
58
59// Returns tag ending at TextPointer
60// (Expects textpointer is currently pointing at the >)
61// and moves TextPointer to the first char of the tag
62Function ExtractPreviousTag( const TextStart: PChar;
63 Var TextPointer: PChar ): TTag;
64
65function ExtractNextTextElement( TextPointer: PChar;
66 Var NextElement: PChar ): TTextElement;
67
68function ExtractPreviousTextElement( const TextStart: PChar;
69 TextPointer: PChar;
70 Var NextElement: PChar ): TTextElement;
71
72// Parse a color name or value (#hexval). Returns true if valid
73function GetTagColor( const ColorParam: string;
74 var Color: TColor ): boolean;
75
76function GetTagTextAlignment( const AlignParam: string;
77 const Default: TTextAlignment ): TTextAlignment;
78
79function GetTagTextWrap( const WrapParam: string ): boolean;
80
81// Search within a rich text document for the given text
82// if found, returns true, pMatch is set to the first match,
83// and MatchLength returns the length of the match
84// (which may be greater than the length of Text due to
85// to skipping tags)
86// if not found, returns false, pMatch is set to nil
87function RichTextFindString( pRichText: PChar;
88 const Text: string;
89 var pMatch: PChar;
90 var MatchLength: longint ): boolean;
91
92// Returns the start of the previous word,
93// or the current word if pStart is in the middle of the word
94function RichTextWordLeft( pRichText: PChar;
95 pStart: PChar ): PChar;
96
97// Returns the start of the next word.
98function RichTextWordRight( pStart: PChar ): PChar;
99
100// If pStart is in the middle of a word, then
101// returns true and sets the start and length of the word
102function RichTextWordAt( pRichText: PChar;
103 pStart: PChar;
104 Var pWordStart: PChar;
105 Var WordLength: longint ): boolean;
106
107// Copies plaintext of richtext starting at StartP
108// to the given buffer. Returns number of characters copied.
109// Buffer may be nil
110// If BufferLength is negative, it is effectively ignored
111function CopyPlainTextToBuffer( StartP: PChar;
112 EndP: PChar;
113 Buffer: PChar;
114 BufferLength: longint ): longint;
115
116Implementation
117
118uses
119 BseDOS, // for NLS/case mapping
120 SysUtils,
121 ACLStringUtility;
122
123const
124 TagStr: array[ ttInvalid .. ttEnd ] of string =
125 (
126 '', //
127 'b',
128 '/b',
129 'i',
130 '/i',
131 'u',
132 '/u',
133 'tt',
134 '/tt',
135 'h1',
136 'h2',
137 'h3',
138 '/h',
139 'color',
140 '/color',
141 'backcolor',
142 '/backcolor',
143 'red',
144 'blue',
145 'green',
146 'black',
147 'wrap',
148 'align',
149 'link',
150 '/link',
151 'leftmargin',
152 'rightmargin',
153 'image',
154 'font',
155 '/font',
156 ''
157 );
158
159 StandardColors: array[ 0..7 ] of TStandardColor =
160 (
161 ( Name : 'white' ; Color: clWhite ),
162 ( Name : 'black' ; Color: clBlack ),
163 ( Name : 'red' ; Color: clRed ),
164 ( Name : 'blue' ; Color: clBlue ),
165 ( Name : 'green' ; Color: clLime ),
166 ( Name : 'purple'; Color: clFuchsia ),
167 ( Name : 'yellow'; Color: clYellow ),
168 ( Name : 'cyan' ; Color: clAqua )
169 );
170
171Procedure ParseTag( const Text: string;
172 Var Tag: TTag );
173var
174 TagType: TTagType;
175 TagTypeText: string;
176 SpacePos: longint;
177begin
178 SpacePos := Pos( ' ', Text );
179 if SpacePos <> 0 then
180 begin
181 Tag.Arguments := trim( Copy( Text, SpacePos + 1, 255 ) );
182 TagTypeText := LowerCase( Copy( Text, 1, SpacePos - 1 ) );
183 end
184 else
185 begin
186 Tag.Arguments := ''; // to save time copying when not needed
187 TagTypeText := LowerCase( Text );
188 end;
189
190 for TagType := ttBold to ttEnd do
191 begin
192 if TagStr[ TagType ] = TagTypeText then
193 begin
194 Tag.TagType := TagType;
195 exit;
196 end;
197 end;
198
199 // not found
200 Tag.TagType := ttInvalid;
201end;
202
203var
204 TagText: string;
205 TagArgText: string;
206
207Function ExtractTag( Var TextPointer: PChar ): TTag;
208var
209 CurrentChar: Char;
210 TagTooLong: boolean;
211 InQuote: boolean;
212begin
213// assert( TextPointer[ 0 ] = '<' );
214 TagText := '';
215 TagTooLong := false;
216 InQuote := false;
217
218 repeat
219 CurrentChar := TextPointer^;
220
221 if ( CurrentChar = '>' )
222 and ( not InQuote ) then
223 begin
224 // found tag end.
225 if TagTooLong then
226 Result.TagType := ttInvalid
227 else
228 ParseTag( TagText, Result );
229 inc( TextPointer );
230 exit;
231 end;
232
233 if CurrentChar = #0 then
234 begin
235 // if we reach here we have reached the end of text
236 // during a tag. invalid tag.
237 Result.TagType := ttInvalid;
238 exit;
239 end;
240
241 if CurrentChar = DoubleQuote then
242 begin
243 if not InQuote then
244 begin
245 InQuote := true
246 end
247 else
248 begin
249 // Could be escaped quote ""
250 if ( TextPointer + 1 ) ^ = DoubleQuote then
251 begin
252 // yes it is
253 inc( TextPointer ); // skip second one
254 end
255 else
256 begin
257 // no, not an escaped quote
258 InQuote := false;
259 end;
260 end;
261
262 end;
263
264 if not TagTooLong then
265 if Length( TagText ) < 200 then
266 TagText := TagText + CurrentChar
267 else
268 TagTooLong := true; // but keep going until the end
269
270 inc( TextPointer );
271 until false;
272
273end;
274
275// Expects textpointer is currently pointing at the >
276Function ExtractPreviousTag( const TextStart: PChar;
277 Var TextPointer: PChar ): TTag;
278var
279 CurrentChar: Char;
280 TagTooLong: boolean;
281 InQuote: boolean;
282begin
283 TagText := '';
284 TagTooLong := false;
285 InQuote := false;
286
287 repeat
288 dec( TextPointer );
289 if TextPointer < TextStart then
290 begin
291 // if we reach here we have reached the end of text
292 // during a tag. invalid tag.
293 Result.TagType := ttInvalid;
294 exit;
295 end;
296 CurrentChar := TextPointer^;
297
298 if ( CurrentChar = '<' )
299 and ( not InQuote ) then
300 begin
301 // found tag end.
302 if TagTooLong then
303 Result.TagType := ttInvalid
304 else
305 ParseTag( TagText, Result );
306 exit;
307 end;
308
309 if CurrentChar = DoubleQuote then
310 begin
311 if not InQuote then
312 begin
313 InQuote := true
314 end
315 else
316 begin
317 // Could be escaped quote ""
318 if TextPointer <= TextStart then
319 begin
320 // start of text... somethin weird
321 InQuote := false;
322 end
323 else if ( TextPointer - 1 ) ^ = DoubleQuote then
324 begin
325 // yes it is
326 dec( TextPointer ); // skip second one
327 end
328 else
329 begin
330 // no, not an escaped quote
331 InQuote := false;
332 end;
333 end;
334
335 end;
336
337 if not TagTooLong then
338 if Length( TagText ) < 200 then
339 TagText := CurrentChar + TagText
340 else
341 TagTooLong := true; // but keep going until the end
342
343 until false;
344
345end;
346
347function ExtractNextTextElement( TextPointer: PChar;
348 Var NextElement: PChar ): TTextElement;
349var
350 TheChar: Char;
351 NextChar: char;
352begin
353 with Result do
354 begin
355 TheChar := TextPointer^;
356 Character := TheChar;
357 inc( TextPointer );
358
359 case TheChar of
360 ' ': // ---- Space (word break) found ----
361 ElementType := teWordBreak;
362
363 #10, #13: // ---- End of line found ----
364 begin
365 ElementType := teLineBreak;
366 if TheChar = #13 then
367 begin
368 TheChar := TextPointer^;
369 if TheChar = #10 then
370 // skip CR following LF
371 inc( TextPointer );
372 end;
373 end;
374
375 #0: // ---- end of text found ----
376 ElementType := teTextEnd;
377
378 '<': // ---- tag found? ----
379 begin
380 NextChar := TextPointer^;
381 if NextChar = '<' then
382 begin
383 // no. just a literal <
384 ElementType := teText;
385 inc( TextPointer );
386 end
387 else
388 begin
389 Tag := ExtractTag( TextPointer );
390 if Tag.TagType = ttImage then
391 ElementType := teImage
392 else
393 ElementType := teStyle;
394 end;
395
396 end;
397
398 '>': // check - should be double
399 begin
400 ElementType := teText;
401 NextChar := TextPointer^;
402 if NextChar = '>' then
403 inc( TextPointer );
404 end;
405
406 else
407 ElementType := teText;
408 end;
409 end; // with
410 NextElement := TextPointer;
411end;
412
413function ExtractPreviousTextElement( const TextStart: PChar;
414 TextPointer: PChar;
415 Var NextElement: PChar ): TTextElement;
416var
417 TheChar: Char;
418 PreviousChar: Char;
419 FoundTag: boolean;
420begin
421 with Result do
422 begin
423 dec( TextPointer );
424 TheChar := TextPointer^;
425 Character := TheChar;
426 if TextPointer < TextStart then
427 begin
428 ElementType := teTextEnd;
429 exit;
430 end;
431
432 case TheChar of
433 ' ': // ---- Space (word break) found ----
434 ElementType := teWordBreak;
435
436 #10, #13: // ---- End of line found ----
437 begin
438 ElementType := teLineBreak;
439 if TheChar = #10 then
440 begin
441 dec( TextPointer );
442 TheChar := TextPointer^;
443 if TheChar = #13 then
444 begin
445 // skip CR preceeding LF
446 end
447 else
448 inc( TextPointer );
449 end;
450 end;
451
452 '>': // ---- tag found ----
453 begin
454 FoundTag := true;
455 if TextPointer > TextStart then
456 begin
457 PreviousChar := ( TextPointer - 1 )^;
458 if PreviousChar = '>' then
459 begin
460 // no. just a literal >
461 FoundTag := false;
462 ElementType := teText;
463 dec( TextPointer );
464 end
465 end;
466
467 if FoundTag then
468 begin
469 Tag := ExtractPreviousTag( TextStart, TextPointer );
470 if Tag.TagType = ttImage then
471 ElementType := teImage
472 else
473 ElementType := teStyle;
474 end;
475 end;
476
477 '<': // should be double
478 begin
479 ElementType := teText;
480 if TextPointer > TextStart then
481 begin
482 PreviousChar := TextPointer^;
483 if PreviousChar = '<' then
484 dec( TextPointer );
485 end;
486 end
487 else
488 ElementType := teText;
489 end;
490 end; // with
491 NextElement := TextPointer;
492end;
493
494function GetTagColor( const ColorParam: string;
495 var Color: TColor ): boolean;
496var
497 ColorIndex: longint;
498begin
499 Result := false;
500 if ColorParam <> '' then
501 begin
502 if ColorParam[ 1 ] = '#' then
503 begin
504 try
505 Color := HexToInt( StrRightFrom( ColorParam, 2 ) );
506 Result := true;
507 except
508 end;
509 end
510 else
511 begin
512 for ColorIndex := 0 to High( StandardColors ) do
513 begin
514 if StringsSame( ColorParam, StandardColors[ ColorIndex ].Name ) then
515 begin
516 Color := StandardColors[ ColorIndex ].Color;
517 Result := true;
518 break;
519 end;
520 end;
521 end;
522 end;
523end;
524
525function GetTagTextAlignment( const AlignParam: string;
526 const Default: TTextAlignment ): TTextAlignment;
527begin
528 if StringsSame( AlignParam, 'left' ) then
529 Result := taLeft
530 else if StringsSame( AlignParam, 'center' ) then
531 Result := taCenter
532 else if StringsSame( AlignParam, 'right' ) then
533 Result := taRight
534 else
535 Result := Default;
536end;
537
538function GetTagTextWrap( const WrapParam: string ): boolean;
539begin
540 Result := StringsSame( WrapParam, 'yes' );
541end;
542
543function RichTextFindString( pRichText: PChar;
544 const Text: string;
545 var pMatch: PChar;
546 var MatchLength: longint ): boolean;
547var
548 P: PChar;
549 NextP: PChar;
550 Element: TTextElement;
551 pMatchStart: pchar;
552 pMatchStartNext: pchar;
553 MatchIndex: longint;
554
555 CountryData: COUNTRYCODE;
556 CaseMap: array[ Low( Char )..High( Char ) ] of char;
557 C: Char;
558begin
559 if Length( Text ) = 0 then
560 begin
561 // null string always matches
562 Result := true;
563 pMatch := pRichText;
564 MatchLength := 0;
565 exit;
566 end;
567
568 P := pRichText;
569
570 MatchIndex := 1;
571
572 // Get case mapping of all chars (only SBCS)
573
574 CountryData.Country := 0; // default country
575 CountryData.CodePage := 0; // default codepage
576
577 // fill array with all chars
578 for C := Low( CaseMap ) to High( CaseMap ) do
579 CaseMap[ C ] := C;
580
581 DosMapCase( sizeof( CaseMap ),
582 CountryData,
583 CaseMap );
584
585 // Now search, case insensitively
586
587 while true do
588 begin
589 Element := ExtractNextTextElement( P, NextP );
590
591 case Element.ElementType of
592 teTextEnd:
593 // end of text
594 break;
595
596 teImage,
597 teLineBreak:
598 // breaks a potential match
599 MatchIndex := 1;
600
601 teStyle:
602 ; // ignore, matches can continue
603
604 else
605 begin
606 if CaseMap[ Element.Character ]
607 = CaseMap[ Text[ MatchIndex ] ] then
608 begin
609 // found a match
610 if MatchIndex = 1 then
611 begin
612 pMatchStart := P; // store start of match
613 pMatchStartNext := NextP;
614 end;
615
616 inc( MatchIndex );
617 if MatchIndex > Length( Text ) then
618 begin
619 // found a complete match
620 Result := true;
621 pMatch := pMatchStart;
622 MatchLength := PCharDiff( P, pMatchStart )
623 + 1; // include this char
624 exit;
625 end;
626 end
627 else
628 begin
629 // not a match
630 if MatchIndex > 1 then
631 begin
632 // go back to start of match, + 1
633 NextP := pMatchStartNext;
634 MatchIndex := 1;
635 end;
636 end;
637 end;
638 end;
639
640 P := NextP;
641 end;
642
643 // no match found
644 Result := false;
645 pMatch := nil;
646 MatchLength := 0;
647end;
648
649function RichTextWordLeft( pRichText: PChar;
650 pStart: PChar ): PChar;
651Var
652 P: PChar;
653 NextP: PChar;
654 Element: TTextElement;
655begin
656 P := pStart;
657
658 // skip whitespace/tags...
659 Element := ExtractPreviousTextElement( pRichText, P, NextP );
660 P := NextP;
661 while Element.ElementType in [ teWordBreak, teLineBreak, teImage, teStyle ] do
662 begin
663 Element := ExtractPreviousTextElement( pRichText, P, NextP );
664 P := NextP;
665 end;
666 if Element.ElementType = teTextEnd then
667 begin
668 Result := P;
669 // out of text
670 exit;
671 end;
672
673 // back to start of word, skip text/tags
674 while true do
675 begin
676 Element := ExtractPreviousTextElement( pRichText, P, NextP );
677 if not ( Element.ElementType in [ teText, teStyle ] ) then
678 break;
679 P := NextP;
680 end;
681 Result := P;
682end;
683
684function RichTextWordRight( pStart: PChar ): PChar;
685Var
686 P: PChar;
687 NextP: PChar;
688 Element: TTextElement;
689begin
690 P := pStart;
691
692 // skip text/tags...
693 Element := ExtractNextTextElement( P, NextP );
694 while Element.ElementType in [ teStyle, teText ] do
695 begin
696 P := NextP;
697 Element := ExtractNextTextElement( P, NextP );
698 end;
699 if Element.ElementType <> teTextEnd then
700 begin
701 // skip whitespace
702 Element := ExtractNextTextElement( P, NextP );
703 while Element.ElementType in [ teWordBreak, teLineBreak, teImage, teStyle ] do
704 begin
705 P := NextP;
706 Element := ExtractNextTextElement( P, NextP );
707 end;
708 end;
709
710 Result := P;
711end;
712
713function RichTextWordAt( pRichText: PChar;
714 pStart: PChar;
715 Var pWordStart: PChar;
716 Var WordLength: longint ): boolean;
717Var
718 P: PChar;
719 NextP: PChar;
720 Element: TTextElement;
721 pWordEnd: PChar;
722begin
723 P := pStart;
724 Element := ExtractNextTextElement( P, NextP );
725 if not ( Element.ElementType in [ teStyle, teText ] ) then
726 begin
727 // not in a word.
728 result := false;
729 pWordStart := nil;
730 WordLength := 0;
731 exit;
732 end;
733 // find end of the word
734 while Element.ElementType in [ teStyle, teText ] do
735 begin
736 P := NextP;
737 Element := ExtractNextTextElement( P, NextP );
738 end;
739 pWordEnd := P;
740
741 P := pStart;
742 Element := ExtractPreviousTextElement( pRichText, P, NextP );
743 while Element.ElementType in [ teStyle, teText ] do
744 begin
745 P := NextP;
746 Element := ExtractPreviousTextElement( pRichText, P, NextP );
747 end;
748 pWordStart := P;
749 WordLength := PCharDiff( pWordEnd, pWordStart );
750 Result := true;
751end;
752
753function CopyPlainTextToBuffer( StartP: PChar;
754 EndP: PChar;
755 Buffer: PChar;
756 BufferLength: longint ): longint;
757var
758 Q: PChar;
759 EndQ: Pchar;
760 P: PChar;
761 NextP: PChar;
762 Element: TTextElement;
763begin
764 P := StartP;
765 Q := Buffer;
766 EndQ := Buffer + BufferLength;
767
768 while P < EndP do
769 begin
770 Element := ExtractNextTextElement( P, NextP );
771 case Element.ElementType of
772 teText, teWordBreak:
773 begin
774 // copy char
775 if Buffer <> nil then
776 Q[ 0 ] := Element.Character;
777 inc( Q );
778 end;
779
780 teLineBreak:
781 begin
782 if Buffer <> nil then
783 Q[ 0 ] := #13;
784 inc( Q );
785 if Q = EndQ then
786 // end of buffer
787 break;
788
789 if Buffer <> nil then
790 Q[ 0 ] := #10;
791 inc( Q );
792 end;
793 end;
794
795 if Q = EndQ then
796 // end of buffer
797 break;
798
799 P := NextP;
800 end;
801 result := PCharDiff( Q, Buffer );
802end;
803
804Initialization
805End.
Note: See TracBrowser for help on using the repository browser.