11/**
22 * MdParagraph Component
3- * Renders a paragraph of text with inline formatting
3+ * Renders a paragraph of text with inline formatting using FormattedString
4+ * for proper text flow and wrapping
45 */
5- import { Component , NO_ERRORS_SCHEMA , ChangeDetectionStrategy , computed , input } from '@angular/core' ;
6+ import { Component , NO_ERRORS_SCHEMA , ChangeDetectionStrategy , computed , input , signal , effect } from '@angular/core' ;
67import { NativeScriptCommonModule } from '@nativescript/angular' ;
8+ import { FormattedString , Span , Label , Color } from '@nativescript/core' ;
79import { MarkdownToken , parseInlineFormatting } from '@nstudio/nstreamdown' ;
810import { openUrl } from '@nstudio/nstreamdown' ;
911
1012@Component ( {
1113 selector : 'MdParagraph' ,
1214 template : `
1315 <StackLayout class="mb-3">
14- <FlexboxLayout flexWrap="wrap" alignItems="center">
15- @for (token of displayTokens(); track $index) {
16- @switch (token.type) {
17- @case ('text') {
18- <Label [text]="token.content" class="text-sm text-slate-700 dark:text-slate-300 leading-[3]" textWrap="true"></Label>
19- }
20- @case ('bold') {
21- <Label [text]="token.content" class="text-sm text-slate-800 dark:text-slate-100 font-bold leading-[3]" textWrap="true"></Label>
22- }
23- @case ('italic') {
24- <Label [text]="token.content" class="text-sm text-slate-700 dark:text-slate-300 italic leading-[3]" textWrap="true"></Label>
25- }
26- @case ('bold-italic') {
27- <Label [text]="token.content" class="text-sm text-slate-800 dark:text-slate-100 font-bold italic leading-[3]" textWrap="true"></Label>
28- }
29- @case ('strikethrough') {
30- <Label [text]="token.content" class="text-sm text-slate-400 dark:text-slate-500 leading-[3]" textDecoration="line-through" textWrap="true"></Label>
31- }
32- @case ('code-inline') {
33- <Label [text]="token.content" class="text-xs font-mono bg-slate-100 dark:bg-slate-700 text-pink-600 dark:text-pink-400 rounded px-1" textWrap="true"></Label>
34- }
35- @case ('link') {
36- <Label [text]="token.content" class="text-sm text-blue-600 dark:text-blue-400 leading-[3]" textDecoration="underline" (tap)="onLinkTap(token)" textWrap="true"></Label>
37- }
38- @case ('math-inline') {
39- <Label [text]="token.content" class="text-sm font-mono text-purple-700 dark:text-purple-300 bg-purple-50 dark:bg-purple-900 rounded px-1" textWrap="true"></Label>
40- }
41- }
42- }
43- </FlexboxLayout>
16+ <Label [formattedText]="formattedString()" textWrap="true" class="text-sm text-slate-700 dark:text-slate-300 leading-[3]" (tap)="onTap($event)"></Label>
4417 </StackLayout>
4518 ` ,
4619 imports : [ NativeScriptCommonModule ] ,
@@ -51,6 +24,9 @@ export class MdParagraph {
5124 content = input ( '' ) ;
5225 children = input < MarkdownToken [ ] > ( [ ] ) ;
5326
27+ // Store link metadata for tap handling
28+ private linkUrls : Map < number , string > = new Map ( ) ;
29+
5430 displayTokens = computed ( ( ) => {
5531 const kids = this . children ( ) ;
5632 const txt = this . content ( ) ;
@@ -62,6 +38,72 @@ export class MdParagraph {
6238 return [ ] ;
6339 } ) ;
6440
41+ formattedString = computed ( ( ) => {
42+ const tokens = this . displayTokens ( ) ;
43+ const fs = new FormattedString ( ) ;
44+ this . linkUrls . clear ( ) ;
45+
46+ tokens . forEach ( ( token , index ) => {
47+ const span = new Span ( ) ;
48+ span . text = token . content ;
49+
50+ switch ( token . type ) {
51+ case 'bold' :
52+ span . fontWeight = 'bold' ;
53+ break ;
54+ case 'italic' :
55+ span . fontStyle = 'italic' ;
56+ break ;
57+ case 'bold-italic' :
58+ span . fontWeight = 'bold' ;
59+ span . fontStyle = 'italic' ;
60+ break ;
61+ case 'strikethrough' :
62+ span . textDecoration = 'line-through' ;
63+ span . color = new Color ( '#94a3b8' ) ; // slate-400
64+ break ;
65+ case 'code-inline' :
66+ span . fontFamily = 'monospace' ;
67+ span . backgroundColor = new Color ( '#f1f5f9' ) ; // slate-100
68+ span . color = new Color ( '#db2777' ) ; // pink-600
69+ break ;
70+ case 'link' :
71+ span . color = new Color ( '#2563eb' ) ; // blue-600
72+ span . textDecoration = 'underline' ;
73+ // Store the URL for this span index
74+ const url = token . metadata ?. [ 'url' ] as string ;
75+ if ( url && url !== 'streamdown:incomplete-link' ) {
76+ this . linkUrls . set ( index , url ) ;
77+ }
78+ break ;
79+ case 'math-inline' :
80+ span . fontFamily = 'monospace' ;
81+ span . color = new Color ( '#7c3aed' ) ; // purple-600
82+ break ;
83+ default :
84+ // text - use default styling
85+ break ;
86+ }
87+
88+ fs . spans . push ( span ) ;
89+ } ) ;
90+
91+ return fs ;
92+ } ) ;
93+
94+ onTap ( args : any ) {
95+ // Handle link taps by checking which span was tapped
96+ // For now, if there's only one link, open it
97+ if ( this . linkUrls . size === 1 ) {
98+ const url = this . linkUrls . values ( ) . next ( ) . value ;
99+ if ( url ) {
100+ openUrl ( url ) ;
101+ }
102+ }
103+ // TODO: For multiple links, we'd need to determine which span was tapped
104+ // This would require native touch handling to get the tapped character position
105+ }
106+
65107 onLinkTap ( token : MarkdownToken ) {
66108 const url = token . metadata ?. [ 'url' ] as string ;
67109 if ( url && url !== 'streamdown:incomplete-link' ) {
0 commit comments