source: branches/2.19_branch/Components/RichTextDocumentUnit.pas@ 324

Last change on this file since 324 was 15, checked in by RBRi, 19 years ago

+ components stuff

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