Skip to content

Commit b0b3905

Browse files
committed
cleanup
1 parent 8627c87 commit b0b3905

File tree

12 files changed

+694
-26
lines changed

12 files changed

+694
-26
lines changed

apps/demo-angular/src/app/demos/home-demo.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,16 +20,16 @@ interface DemoItem {
2020
<GridLayout rows="auto, *" class="bg-slate-50 dark:bg-slate-900">
2121
<!-- Native ActionBar -->
2222
<ActionBar flat="true" class="bg-slate-50 dark:bg-slate-900">
23-
<Label text="Streamdown ~ NativeScript" class="text-lg font-bold text-slate-800 dark:text-slate-100"></Label>
23+
<Label text="@nstudio/nstreamdown" class="text-lg font-bold text-slate-500 dark:text-slate-100"></Label>
2424
</ActionBar>
2525
2626
<!-- Content -->
2727
<ScrollView row="1">
2828
<StackLayout class="p-4 pt-6">
2929
<!-- Hero section -->
3030
<StackLayout class="bg-gradient-to-br from-blue-600 to-purple-700 dark:from-blue-700 dark:to-purple-800 rounded-2xl p-6 mb-6">
31-
<Label text="Native iOS Markdown Streaming" class="text-xl text-white font-bold text-center leading-[3]"></Label>
32-
<Label text="Real-time AI streaming with beautiful markdown rendering, powered by NativeScript." class="text-sm text-blue-100 dark:text-blue-200 text-center leading-[3] mt-2" textWrap="true"></Label>
31+
<Label text="Native Markdown Streaming" class="text-xl dark:text-white text-black font-bold text-center leading-[3]"></Label>
32+
<Label text="Real-time AI streaming with beautiful markdown rendering, powered by NativeScript." class="text-sm text-blue-400 dark:text-blue-200 text-center leading-[3] mt-2" textWrap="true"></Label>
3333
</StackLayout>
3434
3535
<!-- Demo cards -->
@@ -44,9 +44,9 @@ interface DemoItem {
4444
</GridLayout>
4545
4646
<!-- Text - use nested GridLayout for vertical centering -->
47-
<GridLayout col="1" rows="auto, auto" class="ml-3">
48-
<Label row="0" [text]="demo.title" class="text-base font-semibold text-slate-800 dark:text-slate-100 leading-[3]"></Label>
49-
<Label row="1" [text]="demo.description" class="text-xs text-slate-500 dark:text-slate-400 leading-[3]" textWrap="true"></Label>
47+
<GridLayout col="1" rows="*,auto, auto,*" class="ml-3">
48+
<Label row="1" [text]="demo.title" class="text-base font-semibold text-slate-800 dark:text-slate-100 leading-[3]"></Label>
49+
<Label row="2" [text]="demo.description" class="text-xs text-slate-500 dark:text-slate-400 leading-[3]" textWrap="true"></Label>
5050
</GridLayout>
5151
5252
<!-- Arrow -->
@@ -97,7 +97,7 @@ export class HomeDemo {
9797
},
9898
];
9999

100-
features = ['Streaming markdown', 'Incomplete tokens', 'GFM support', 'Code highlighting', 'Tables', 'Math (LaTeX)', 'Images', 'Native performance', 'Dark mode', 'CJK support'];
100+
features = ['Stream markdown', 'Incomplete tokens', 'GFM support', 'Code highlighting', 'Tables', 'Math (LaTeX)', 'Images', 'Native performance', 'Dark mode', 'CJK support'];
101101

102102
getFeatureRow(index: number): number {
103103
return Math.floor(index / 2);

apps/demo-angular/src/app/demos/streamdown-demo.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import type { StreamdownConfig } from '@nstudio/nstreamdown/angular';
1111
// Sample AI response that demonstrates all markdown features
1212
const DEMO_MARKDOWN = `# NativeScript Streamdown 🚀
1313
14-
This is a **native iOS** implementation of [streamdown.ai](https://streamdown.ai), designed for real-time AI streaming content.
14+
This is a **native** implementation of [streamdown.ai](https://streamdown.ai), designed for real-time AI streaming content.
1515
1616
## Key Features
1717
@@ -57,8 +57,7 @@ class ViewController: UIViewController {
5757
|---------|------------|----------------|
5858
| Streaming | ✅ Yes | ❌ No |
5959
| Incomplete Tokens | ✅ Handles | ❌ Breaks |
60-
| Native iOS | ✅ Yes | ❌ Web Only |
61-
| Performance | ⚡ Fast | 🐢 Slower |
60+
| Native | ✅ Yes | ❌ Web Only |
6261
6362
## Blockquotes
6463
@@ -100,7 +99,9 @@ Streamdown supports GFM features like:
10099
101100
## CJK Language Support
102101
103-
日本語、中文、한국어 are fully supported with proper emphasis handling.
102+
**日本語、中文、한국어** are fully supported with proper emphasis handling.
103+
104+
~~日本語、中文、한국어~~ or with strikethrough handling.
104105
105106
---
106107

apps/demo-react/src/components/Home.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ export const Home = ({ onNavigate }: HomeProps) => {
3838
return (
3939
<page>
4040
<actionBar flat={true} className="bg-slate-50">
41-
<label text="Streamdown ~ NativeScript" className="text-lg font-bold text-slate-800" />
41+
<label text="nstreamdown" className="text-lg font-bold text-slate-800" />
4242
</actionBar>
4343

4444
<gridLayout rows="*" className="bg-slate-50">

apps/demo-vue/src/components/Home.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ function navigateTo(route: 'demo' | 'chat') {
4646
<template>
4747
<Page>
4848
<ActionBar flat="true" class="bg-slate-50">
49-
<Label text="Streamdown ~ NativeScript" class="text-lg font-bold text-slate-800" />
49+
<Label text="nstreamdown" class="text-lg font-bold text-slate-800" />
5050
</ActionBar>
5151

5252
<GridLayout rows="*" class="bg-slate-50">

packages/nstreamdown/README.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,22 @@ Native iOS/Android streaming markdown rendering for NativeScript, inspired by [s
77
- 🚀 **Real-time streaming** - Renders markdown as it streams in from AI/LLM APIs
88
-**Native performance** - Uses native iOS/Android views for smooth scrolling
99
- 📝 **GFM support** - GitHub Flavored Markdown with tables, task lists, strikethrough
10-
- 🎨 **Syntax highlighting** - Native code highlighting using iOS NSAttributedString
10+
- 🎨 **Syntax highlighting** - Native code highlighting using iOS NSAttributedString and Android SpannableString
1111
- 🧮 **Math expressions** - LaTeX to Unicode math rendering
1212
- 📱 **Platform optimized** - SF Symbols on iOS, Material icons on Android
1313

14+
## Platform Support
15+
16+
### iOS
17+
- Uses `UITextView` with `NSAttributedString` for rich text rendering
18+
- Native `SyntaxHighlighter.swift` for performant syntax highlighting
19+
- Full support for all markdown features
20+
21+
### Android
22+
- Uses `TextView` with `SpannableString` for rich text rendering
23+
- Native `SyntaxHighlighter.kt` (Kotlin) for performant syntax highlighting
24+
- Full support for all markdown features
25+
1426
## Installation
1527

1628
```bash

packages/nstreamdown/lib/utils.ts

Lines changed: 81 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -116,14 +116,25 @@ export function cn(...classes: (string | undefined | null | false)[]): string {
116116
return classes.filter(Boolean).join(' ');
117117
}
118118

119+
// Declare Android types for rich text
120+
declare const android: any;
121+
119122
/**
120-
* Create iOS attributed string for rich text
123+
* Create attributed/spannable string for rich text (iOS and Android)
121124
*/
122125
export function createAttributedString(text: string, attributes: Record<string, unknown>): any {
123-
if (!global.isIOS) {
124-
return null;
126+
if (global.isIOS) {
127+
return createIOSAttributedString(text, attributes);
128+
} else if (global.isAndroid) {
129+
return createAndroidSpannableString(text, attributes);
125130
}
131+
return null;
132+
}
126133

134+
/**
135+
* Create iOS attributed string for rich text
136+
*/
137+
function createIOSAttributedString(text: string, attributes: Record<string, unknown>): any {
127138
const attributedString = NSMutableAttributedString.alloc().initWithString(text);
128139

129140
if (attributes.bold) {
@@ -157,6 +168,49 @@ export function createAttributedString(text: string, attributes: Record<string,
157168
return attributedString;
158169
}
159170

171+
/**
172+
* Create Android SpannableString for rich text
173+
*/
174+
function createAndroidSpannableString(text: string, attributes: Record<string, unknown>): any {
175+
const spannableString = new android.text.SpannableStringBuilder(text);
176+
177+
// Bold
178+
if (attributes.bold) {
179+
spannableString.setSpan(new android.text.style.StyleSpan(android.graphics.Typeface.BOLD), 0, text.length, android.text.Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
180+
}
181+
182+
// Italic
183+
if (attributes.italic) {
184+
spannableString.setSpan(new android.text.style.StyleSpan(android.graphics.Typeface.ITALIC), 0, text.length, android.text.Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
185+
}
186+
187+
// Foreground color
188+
if (attributes.color) {
189+
const c = attributes.color as Color;
190+
const androidColor = android.graphics.Color.argb(c.a || 255, c.r || 0, c.g || 0, c.b || 0);
191+
spannableString.setSpan(new android.text.style.ForegroundColorSpan(androidColor), 0, text.length, android.text.Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
192+
}
193+
194+
// Background color
195+
if (attributes.backgroundColor) {
196+
const c = attributes.backgroundColor as Color;
197+
const androidColor = android.graphics.Color.argb(c.a || 255, c.r || 0, c.g || 0, c.b || 0);
198+
spannableString.setSpan(new android.text.style.BackgroundColorSpan(androidColor), 0, text.length, android.text.Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
199+
}
200+
201+
// Strikethrough
202+
if (attributes.strikethrough) {
203+
spannableString.setSpan(new android.text.style.StrikethroughSpan(), 0, text.length, android.text.Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
204+
}
205+
206+
// Underline
207+
if (attributes.underline) {
208+
spannableString.setSpan(new android.text.style.UnderlineSpan(), 0, text.length, android.text.Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
209+
}
210+
211+
return spannableString;
212+
}
213+
160214
/**
161215
* Format font size based on heading level
162216
*/
@@ -364,18 +418,29 @@ export function debounce<T extends (...args: any[]) => any>(fn: T, delay: number
364418
}
365419

366420
/**
367-
* Copy text to clipboard (iOS)
421+
* Copy text to clipboard (iOS and Android)
368422
*/
369423
export function copyToClipboard(text: string): boolean {
370424
if (global.isIOS) {
371425
UIPasteboard.generalPasteboard.string = text;
372426
return true;
427+
} else if (global.isAndroid) {
428+
try {
429+
const context = Utils.android.getApplicationContext();
430+
const clipboard = context.getSystemService(android.content.Context.CLIPBOARD_SERVICE);
431+
const clip = android.content.ClipData.newPlainText('text', text);
432+
clipboard.setPrimaryClip(clip);
433+
return true;
434+
} catch (e) {
435+
console.error('Failed to copy to clipboard:', e);
436+
return false;
437+
}
373438
}
374439
return false;
375440
}
376441

377442
/**
378-
* Open URL in Safari
443+
* Open URL in browser (iOS Safari, Android default browser)
379444
*/
380445
export function openUrl(url: string): boolean {
381446
if (global.isIOS) {
@@ -384,6 +449,17 @@ export function openUrl(url: string): boolean {
384449
UIApplication.sharedApplication.openURLOptionsCompletionHandler(nsUrl, null, null);
385450
return true;
386451
}
452+
} else if (global.isAndroid) {
453+
try {
454+
const context = Utils.android.getApplicationContext();
455+
const intent = new android.content.Intent(android.content.Intent.ACTION_VIEW, android.net.Uri.parse(url));
456+
intent.addFlags(android.content.Intent.FLAG_ACTIVITY_NEW_TASK);
457+
context.startActivity(intent);
458+
return true;
459+
} catch (e) {
460+
console.error('Failed to open URL:', e);
461+
return false;
462+
}
387463
}
388464
return false;
389465
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
android {
2+
defaultConfig {
3+
minSdkVersion 21
4+
}
5+
}
6+
7+
dependencies {
8+
// AndroidX for SpannableString and text handling
9+
implementation 'androidx.core:core-ktx:1.12.0'
10+
implementation 'androidx.appcompat:appcompat:1.6.1'
11+
}

0 commit comments

Comments
 (0)