mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-05-03 16:59:08 +00:00
ksplayer word splitting fix
This commit is contained in:
parent
0ab85ec870
commit
ecaaaa66ed
3 changed files with 289 additions and 291 deletions
|
|
@ -328,7 +328,7 @@ const KSPlayerCore: React.FC = () => {
|
||||||
id: id || 'placeholder',
|
id: id || 'placeholder',
|
||||||
type: type || 'movie'
|
type: type || 'movie'
|
||||||
});
|
});
|
||||||
const { metadata, loading: metadataLoading, groupedEpisodes: metadataGroupedEpisodes, cast, loadCast } = shouldLoadMetadata ? (metadataResult as any) : { metadata: null, loading: false, groupedEpisodes: {}, cast: [], loadCast: () => {} };
|
const { metadata, loading: metadataLoading, groupedEpisodes: metadataGroupedEpisodes, cast, loadCast } = shouldLoadMetadata ? (metadataResult as any) : { metadata: null, loading: false, groupedEpisodes: {}, cast: [], loadCast: () => { } };
|
||||||
const { settings } = useSettings();
|
const { settings } = useSettings();
|
||||||
|
|
||||||
// Logo animation values
|
// Logo animation values
|
||||||
|
|
@ -559,7 +559,7 @@ const KSPlayerCore: React.FC = () => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
if (isSpeedBoosted) {
|
if (isSpeedBoosted) {
|
||||||
try { setPlaybackSpeed(originalSpeed); } catch {}
|
try { setPlaybackSpeed(originalSpeed); } catch { }
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [isSpeedBoosted, originalSpeed]);
|
}, [isSpeedBoosted, originalSpeed]);
|
||||||
|
|
@ -648,7 +648,7 @@ const KSPlayerCore: React.FC = () => {
|
||||||
if (isOpeningAnimationComplete) {
|
if (isOpeningAnimationComplete) {
|
||||||
enableImmersiveMode();
|
enableImmersiveMode();
|
||||||
}
|
}
|
||||||
return () => {};
|
return () => { };
|
||||||
}, [isOpeningAnimationComplete])
|
}, [isOpeningAnimationComplete])
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -1011,12 +1011,12 @@ const KSPlayerCore: React.FC = () => {
|
||||||
const trackLang = (track.language || '').toLowerCase();
|
const trackLang = (track.language || '').toLowerCase();
|
||||||
// Prefer stereo, AAC, or standard audio formats, avoid heavy codecs
|
// Prefer stereo, AAC, or standard audio formats, avoid heavy codecs
|
||||||
return !trackName.includes('truehd') &&
|
return !trackName.includes('truehd') &&
|
||||||
!trackName.includes('dts') &&
|
!trackName.includes('dts') &&
|
||||||
!trackName.includes('dolby') &&
|
!trackName.includes('dolby') &&
|
||||||
!trackName.includes('atmos') &&
|
!trackName.includes('atmos') &&
|
||||||
!trackName.includes('7.1') &&
|
!trackName.includes('7.1') &&
|
||||||
!trackName.includes('5.1') &&
|
!trackName.includes('5.1') &&
|
||||||
index !== selectedAudioTrack; // Don't select the same track
|
index !== selectedAudioTrack; // Don't select the same track
|
||||||
});
|
});
|
||||||
|
|
||||||
if (fallbackTrack) {
|
if (fallbackTrack) {
|
||||||
|
|
@ -1203,10 +1203,10 @@ const KSPlayerCore: React.FC = () => {
|
||||||
// Auto-select English audio track if available, otherwise first track
|
// Auto-select English audio track if available, otherwise first track
|
||||||
if (selectedAudioTrack === null && formattedAudioTracks.length > 0) {
|
if (selectedAudioTrack === null && formattedAudioTracks.length > 0) {
|
||||||
// Look for English track first
|
// Look for English track first
|
||||||
const englishTrack = formattedAudioTracks.find((track: {id: number, name: string, language?: string}) => {
|
const englishTrack = formattedAudioTracks.find((track: { id: number, name: string, language?: string }) => {
|
||||||
const lang = (track.language || '').toLowerCase();
|
const lang = (track.language || '').toLowerCase();
|
||||||
return lang === 'english' || lang === 'en' || lang === 'eng' ||
|
return lang === 'english' || lang === 'en' || lang === 'eng' ||
|
||||||
(track.name && track.name.toLowerCase().includes('english'));
|
(track.name && track.name.toLowerCase().includes('english'));
|
||||||
});
|
});
|
||||||
|
|
||||||
const selectedTrack = englishTrack || formattedAudioTracks[0];
|
const selectedTrack = englishTrack || formattedAudioTracks[0];
|
||||||
|
|
@ -1248,7 +1248,7 @@ const KSPlayerCore: React.FC = () => {
|
||||||
const lang = (track.language || '').toLowerCase();
|
const lang = (track.language || '').toLowerCase();
|
||||||
const name = (track.name || '').toLowerCase();
|
const name = (track.name || '').toLowerCase();
|
||||||
return lang === 'english' || lang === 'en' || lang === 'eng' ||
|
return lang === 'english' || lang === 'en' || lang === 'eng' ||
|
||||||
name.includes('english') || name.includes('en');
|
name.includes('english') || name.includes('en');
|
||||||
});
|
});
|
||||||
|
|
||||||
if (englishTrack) {
|
if (englishTrack) {
|
||||||
|
|
@ -1393,9 +1393,9 @@ const KSPlayerCore: React.FC = () => {
|
||||||
const isTablet = (Platform as any).isPad === true || Math.min(dw, dh) >= 768;
|
const isTablet = (Platform as any).isPad === true || Math.min(dw, dh) >= 768;
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (isTablet) {
|
if (isTablet) {
|
||||||
ScreenOrientation.unlockAsync().catch(() => {});
|
ScreenOrientation.unlockAsync().catch(() => { });
|
||||||
} else {
|
} else {
|
||||||
ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.PORTRAIT_UP).catch(() => {});
|
ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.PORTRAIT_UP).catch(() => { });
|
||||||
}
|
}
|
||||||
}, 50);
|
}, 50);
|
||||||
}
|
}
|
||||||
|
|
@ -1538,12 +1538,12 @@ const KSPlayerCore: React.FC = () => {
|
||||||
const trackLang = (track.language || '').toLowerCase();
|
const trackLang = (track.language || '').toLowerCase();
|
||||||
// Prefer stereo, AAC, or standard audio formats, avoid heavy codecs
|
// Prefer stereo, AAC, or standard audio formats, avoid heavy codecs
|
||||||
return !trackName.includes('truehd') &&
|
return !trackName.includes('truehd') &&
|
||||||
!trackName.includes('dts') &&
|
!trackName.includes('dts') &&
|
||||||
!trackName.includes('dolby') &&
|
!trackName.includes('dolby') &&
|
||||||
!trackName.includes('atmos') &&
|
!trackName.includes('atmos') &&
|
||||||
!trackName.includes('7.1') &&
|
!trackName.includes('7.1') &&
|
||||||
!trackName.includes('5.1') &&
|
!trackName.includes('5.1') &&
|
||||||
index !== selectedAudioTrack; // Don't select the same track
|
index !== selectedAudioTrack; // Don't select the same track
|
||||||
});
|
});
|
||||||
|
|
||||||
if (fallbackTrack) {
|
if (fallbackTrack) {
|
||||||
|
|
@ -1673,8 +1673,8 @@ const KSPlayerCore: React.FC = () => {
|
||||||
// Check if this is a multi-channel track that might need downmixing
|
// Check if this is a multi-channel track that might need downmixing
|
||||||
const trackName = selectedTrack.name.toLowerCase();
|
const trackName = selectedTrack.name.toLowerCase();
|
||||||
const isMultiChannel = trackName.includes('5.1') || trackName.includes('7.1') ||
|
const isMultiChannel = trackName.includes('5.1') || trackName.includes('7.1') ||
|
||||||
trackName.includes('truehd') || trackName.includes('dts') ||
|
trackName.includes('truehd') || trackName.includes('dts') ||
|
||||||
trackName.includes('dolby') || trackName.includes('atmos');
|
trackName.includes('dolby') || trackName.includes('atmos');
|
||||||
|
|
||||||
if (isMultiChannel) {
|
if (isMultiChannel) {
|
||||||
logger.log(`[VideoPlayer] Multi-channel audio track detected: ${selectedTrack.name}`);
|
logger.log(`[VideoPlayer] Multi-channel audio track detected: ${selectedTrack.name}`);
|
||||||
|
|
@ -1757,9 +1757,9 @@ const KSPlayerCore: React.FC = () => {
|
||||||
try {
|
try {
|
||||||
const merged = { ...(saved || {}), subtitleSize: migrated };
|
const merged = { ...(saved || {}), subtitleSize: migrated };
|
||||||
await storageService.saveSubtitleSettings(merged);
|
await storageService.saveSubtitleSettings(merged);
|
||||||
} catch {}
|
} catch { }
|
||||||
}
|
}
|
||||||
try { await mmkvStorage.removeItem(SUBTITLE_SIZE_KEY); } catch {}
|
try { await mmkvStorage.removeItem(SUBTITLE_SIZE_KEY); } catch { }
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// If no saved settings, use responsive default
|
// If no saved settings, use responsive default
|
||||||
|
|
@ -2196,33 +2196,28 @@ const KSPlayerCore: React.FC = () => {
|
||||||
|
|
||||||
// Extract formatted segments from current cue
|
// Extract formatted segments from current cue
|
||||||
if (currentCue?.formattedSegments) {
|
if (currentCue?.formattedSegments) {
|
||||||
// Split by newlines to get per-line segments
|
|
||||||
const lines = (currentCue.text || '').split(/\r?\n/);
|
|
||||||
const segmentsPerLine: SubtitleSegment[][] = [];
|
const segmentsPerLine: SubtitleSegment[][] = [];
|
||||||
let segmentIndex = 0;
|
let currentLine: SubtitleSegment[] = [];
|
||||||
|
|
||||||
for (const line of lines) {
|
currentCue.formattedSegments.forEach(seg => {
|
||||||
const lineSegments: SubtitleSegment[] = [];
|
const parts = seg.text.split(/\r?\n/);
|
||||||
const words = line.split(/(\s+)/);
|
parts.forEach((part, index) => {
|
||||||
|
if (index > 0) {
|
||||||
for (const word of words) {
|
// New line found
|
||||||
if (word.trim()) {
|
segmentsPerLine.push(currentLine);
|
||||||
if (segmentIndex < currentCue.formattedSegments.length) {
|
currentLine = [];
|
||||||
lineSegments.push(currentCue.formattedSegments[segmentIndex]);
|
|
||||||
segmentIndex++;
|
|
||||||
} else {
|
|
||||||
// Fallback if segment count doesn't match
|
|
||||||
lineSegments.push({ text: word });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
if (part.length > 0) {
|
||||||
|
currentLine.push({ ...seg, text: part });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
if (lineSegments.length > 0) {
|
if (currentLine.length > 0) {
|
||||||
segmentsPerLine.push(lineSegments);
|
segmentsPerLine.push(currentLine);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setCurrentFormattedSegments(segmentsPerLine.length > 0 ? segmentsPerLine : []);
|
setCurrentFormattedSegments(segmentsPerLine);
|
||||||
} else {
|
} else {
|
||||||
setCurrentFormattedSegments([]);
|
setCurrentFormattedSegments([]);
|
||||||
}
|
}
|
||||||
|
|
@ -2243,14 +2238,14 @@ const KSPlayerCore: React.FC = () => {
|
||||||
if (typeof saved.subtitleOutlineColor === 'string') setSubtitleOutlineColor(saved.subtitleOutlineColor);
|
if (typeof saved.subtitleOutlineColor === 'string') setSubtitleOutlineColor(saved.subtitleOutlineColor);
|
||||||
if (typeof saved.subtitleOutlineWidth === 'number') setSubtitleOutlineWidth(saved.subtitleOutlineWidth);
|
if (typeof saved.subtitleOutlineWidth === 'number') setSubtitleOutlineWidth(saved.subtitleOutlineWidth);
|
||||||
if (typeof saved.subtitleAlign === 'string') setSubtitleAlign(saved.subtitleAlign as 'center' | 'left' | 'right');
|
if (typeof saved.subtitleAlign === 'string') setSubtitleAlign(saved.subtitleAlign as 'center' | 'left' | 'right');
|
||||||
if (typeof saved.subtitleBottomOffset === 'number') setSubtitleBottomOffset(saved.subtitleBottomOffset);
|
if (typeof saved.subtitleBottomOffset === 'number') setSubtitleBottomOffset(saved.subtitleBottomOffset);
|
||||||
if (typeof saved.subtitleLetterSpacing === 'number') setSubtitleLetterSpacing(saved.subtitleLetterSpacing);
|
if (typeof saved.subtitleLetterSpacing === 'number') setSubtitleLetterSpacing(saved.subtitleLetterSpacing);
|
||||||
if (typeof saved.subtitleLineHeightMultiplier === 'number') setSubtitleLineHeightMultiplier(saved.subtitleLineHeightMultiplier);
|
if (typeof saved.subtitleLineHeightMultiplier === 'number') setSubtitleLineHeightMultiplier(saved.subtitleLineHeightMultiplier);
|
||||||
if (typeof saved.subtitleOffsetSec === 'number') setSubtitleOffsetSec(saved.subtitleOffsetSec);
|
if (typeof saved.subtitleOffsetSec === 'number') setSubtitleOffsetSec(saved.subtitleOffsetSec);
|
||||||
}
|
}
|
||||||
} catch {} finally {
|
} catch { } finally {
|
||||||
// Mark subtitle settings as loaded so we can safely persist subsequent changes
|
// Mark subtitle settings as loaded so we can safely persist subsequent changes
|
||||||
try { setSubtitleSettingsLoaded(true); } catch {}
|
try { setSubtitleSettingsLoaded(true); } catch { }
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
@ -2283,7 +2278,7 @@ const KSPlayerCore: React.FC = () => {
|
||||||
subtitleOutlineColor,
|
subtitleOutlineColor,
|
||||||
subtitleOutlineWidth,
|
subtitleOutlineWidth,
|
||||||
subtitleAlign,
|
subtitleAlign,
|
||||||
subtitleBottomOffset,
|
subtitleBottomOffset,
|
||||||
subtitleLetterSpacing,
|
subtitleLetterSpacing,
|
||||||
subtitleLineHeightMultiplier,
|
subtitleLineHeightMultiplier,
|
||||||
subtitleOffsetSec,
|
subtitleOffsetSec,
|
||||||
|
|
@ -2690,11 +2685,11 @@ const KSPlayerCore: React.FC = () => {
|
||||||
buffered={buffered}
|
buffered={buffered}
|
||||||
formatTime={formatTime}
|
formatTime={formatTime}
|
||||||
playerBackend={playerBackend}
|
playerBackend={playerBackend}
|
||||||
cyclePlaybackSpeed={cyclePlaybackSpeed}
|
cyclePlaybackSpeed={cyclePlaybackSpeed}
|
||||||
currentPlaybackSpeed={playbackSpeed}
|
currentPlaybackSpeed={playbackSpeed}
|
||||||
isAirPlayActive={isAirPlayActive}
|
isAirPlayActive={isAirPlayActive}
|
||||||
allowsAirPlay={allowsAirPlay}
|
allowsAirPlay={allowsAirPlay}
|
||||||
onAirPlayPress={handleAirPlayPress}
|
onAirPlayPress={handleAirPlayPress}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{showPauseOverlay && (
|
{showPauseOverlay && (
|
||||||
|
|
@ -2725,7 +2720,7 @@ const KSPlayerCore: React.FC = () => {
|
||||||
<LinearGradient
|
<LinearGradient
|
||||||
start={{ x: 0, y: 0.5 }}
|
start={{ x: 0, y: 0.5 }}
|
||||||
end={{ x: 1, y: 0.5 }}
|
end={{ x: 1, y: 0.5 }}
|
||||||
colors={[ 'rgba(0,0,0,0.85)', 'rgba(0,0,0,0.0)' ]}
|
colors={['rgba(0,0,0,0.85)', 'rgba(0,0,0,0.0)']}
|
||||||
locations={[0, 1]}
|
locations={[0, 1]}
|
||||||
style={StyleSheet.absoluteFill}
|
style={StyleSheet.absoluteFill}
|
||||||
/>
|
/>
|
||||||
|
|
@ -2948,38 +2943,38 @@ const KSPlayerCore: React.FC = () => {
|
||||||
marginRight: 8,
|
marginRight: 8,
|
||||||
marginBottom: 8,
|
marginBottom: 8,
|
||||||
}}
|
}}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
setSelectedCastMember(castMember);
|
setSelectedCastMember(castMember);
|
||||||
// Animate metadata out, then cast details in
|
// Animate metadata out, then cast details in
|
||||||
Animated.parallel([
|
|
||||||
Animated.timing(metadataOpacity, {
|
|
||||||
toValue: 0,
|
|
||||||
duration: 250,
|
|
||||||
useNativeDriver: true,
|
|
||||||
}),
|
|
||||||
Animated.timing(metadataScale, {
|
|
||||||
toValue: 0.95,
|
|
||||||
duration: 250,
|
|
||||||
useNativeDriver: true,
|
|
||||||
})
|
|
||||||
]).start(() => {
|
|
||||||
setShowCastDetails(true);
|
|
||||||
// Animate cast details in
|
|
||||||
Animated.parallel([
|
Animated.parallel([
|
||||||
Animated.timing(castDetailsOpacity, {
|
Animated.timing(metadataOpacity, {
|
||||||
toValue: 1,
|
toValue: 0,
|
||||||
duration: 400,
|
duration: 250,
|
||||||
useNativeDriver: true,
|
useNativeDriver: true,
|
||||||
}),
|
}),
|
||||||
Animated.spring(castDetailsScale, {
|
Animated.timing(metadataScale, {
|
||||||
toValue: 1,
|
toValue: 0.95,
|
||||||
tension: 80,
|
duration: 250,
|
||||||
friction: 8,
|
|
||||||
useNativeDriver: true,
|
useNativeDriver: true,
|
||||||
})
|
})
|
||||||
]).start();
|
]).start(() => {
|
||||||
});
|
setShowCastDetails(true);
|
||||||
}}
|
// Animate cast details in
|
||||||
|
Animated.parallel([
|
||||||
|
Animated.timing(castDetailsOpacity, {
|
||||||
|
toValue: 1,
|
||||||
|
duration: 400,
|
||||||
|
useNativeDriver: true,
|
||||||
|
}),
|
||||||
|
Animated.spring(castDetailsScale, {
|
||||||
|
toValue: 1,
|
||||||
|
tension: 80,
|
||||||
|
friction: 8,
|
||||||
|
useNativeDriver: true,
|
||||||
|
})
|
||||||
|
]).start();
|
||||||
|
});
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Text style={{
|
<Text style={{
|
||||||
color: '#FFFFFF',
|
color: '#FFFFFF',
|
||||||
|
|
|
||||||
|
|
@ -65,22 +65,25 @@ export const CustomSubtitles: React.FC<CustomSubtitlesProps> = ({
|
||||||
}
|
}
|
||||||
effectiveBottom = Math.max(0, effectiveBottom);
|
effectiveBottom = Math.max(0, effectiveBottom);
|
||||||
|
|
||||||
// When using crisp outline, prefer SVG text with real stroke instead of blur shadow
|
|
||||||
const useCrispSvgOutline = outline === true;
|
|
||||||
|
|
||||||
const shadowStyle = (textShadow && !useCrispSvgOutline)
|
|
||||||
? {
|
|
||||||
textShadowColor: 'rgba(0, 0, 0, 0.9)',
|
|
||||||
textShadowOffset: { width: 2, height: 2 },
|
|
||||||
textShadowRadius: 4,
|
|
||||||
}
|
|
||||||
: {};
|
|
||||||
|
|
||||||
// Prepare content lines
|
// Prepare content lines
|
||||||
const lines = String(currentSubtitle).split(/\r?\n/);
|
const lines = String(currentSubtitle).split(/\r?\n/);
|
||||||
|
|
||||||
// Detect RTL for each line
|
// Detect RTL for each line
|
||||||
const lineRTLStatus = lines.map(line => detectRTL(line));
|
const lineRTLStatus = lines.map(line => detectRTL(line));
|
||||||
|
const hasRTL = lineRTLStatus.some(status => status);
|
||||||
|
|
||||||
|
// When using crisp outline, prefer SVG text with real stroke instead of blur shadow
|
||||||
|
// However, SVG text does not support complex text shaping (required for Arabic/RTL),
|
||||||
|
// so we must fallback to standard Text component for RTL languages.
|
||||||
|
const useCrispSvgOutline = outline === true && !hasRTL;
|
||||||
|
|
||||||
|
const shadowStyle = (textShadow && !useCrispSvgOutline)
|
||||||
|
? {
|
||||||
|
textShadowColor: 'rgba(0, 0, 0, 0.9)',
|
||||||
|
textShadowOffset: { width: 2, height: 2 },
|
||||||
|
textShadowRadius: 4,
|
||||||
|
}
|
||||||
|
: {};
|
||||||
|
|
||||||
const displayFontSize = subtitleSize * inverseScale;
|
const displayFontSize = subtitleSize * inverseScale;
|
||||||
const displayLineHeight = subtitleSize * lineHeightMultiplier * inverseScale;
|
const displayLineHeight = subtitleSize * lineHeightMultiplier * inverseScale;
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ export const safeDebugLog = (message: string, data?: any) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add language code to name mapping
|
// Add language code to name mapping
|
||||||
export const languageMap: {[key: string]: string} = {
|
export const languageMap: { [key: string]: string } = {
|
||||||
'en': 'English',
|
'en': 'English',
|
||||||
'eng': 'English',
|
'eng': 'English',
|
||||||
'es': 'Spanish',
|
'es': 'Spanish',
|
||||||
|
|
@ -84,7 +84,7 @@ export const formatLanguage = (code?: string): string => {
|
||||||
|
|
||||||
// If the result is still the uppercased code, it means we couldn't find it in our map.
|
// If the result is still the uppercased code, it means we couldn't find it in our map.
|
||||||
if (languageName === code.toUpperCase()) {
|
if (languageName === code.toUpperCase()) {
|
||||||
return `Unknown (${code})`;
|
return `Unknown (${code})`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return languageName;
|
return languageName;
|
||||||
|
|
@ -104,7 +104,7 @@ export const getTrackDisplayName = (track: { name?: string, id: number, language
|
||||||
|
|
||||||
// If the track name contains detailed information (like codec, bitrate, etc.), use it as-is
|
// If the track name contains detailed information (like codec, bitrate, etc.), use it as-is
|
||||||
if (track.name && (track.name.includes('DDP') || track.name.includes('DTS') || track.name.includes('AAC') ||
|
if (track.name && (track.name.includes('DDP') || track.name.includes('DTS') || track.name.includes('AAC') ||
|
||||||
track.name.includes('Kbps') || track.name.includes('Atmos') || track.name.includes('~'))) {
|
track.name.includes('Kbps') || track.name.includes('Atmos') || track.name.includes('~'))) {
|
||||||
return track.name;
|
return track.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -189,7 +189,7 @@ export const detectRTL = (text: string): boolean => {
|
||||||
// Arabic Presentation Forms-B: U+FE70–U+FEFF
|
// Arabic Presentation Forms-B: U+FE70–U+FEFF
|
||||||
// Hebrew: U+0590–U+05FF
|
// Hebrew: U+0590–U+05FF
|
||||||
// Persian/Urdu use Arabic script (no separate range)
|
// Persian/Urdu use Arabic script (no separate range)
|
||||||
const rtlRegex = /[\u0590-\u05FF\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF\uFB50-\uFDFF\uFE70-\uFEFF]/;
|
const rtlRegex = /[\u0590-\u05FF\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF\uFB50-\uFDFF\uFE70-\uFEFF]/g;
|
||||||
|
|
||||||
// Remove whitespace and count characters
|
// Remove whitespace and count characters
|
||||||
const nonWhitespace = text.replace(/\s/g, '');
|
const nonWhitespace = text.replace(/\s/g, '');
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue