diff --git a/app/Editor.tsx b/app/Editor.tsx new file mode 100644 index 0000000..e303009 --- /dev/null +++ b/app/Editor.tsx @@ -0,0 +1,240 @@ +import { format } from 'date-fns'; +import { Appbar, Button, Dialog, Menu, Portal, Text, useTheme } from 'react-native-paper'; +import { marked } from 'marked'; +import turndown from 'turndown-rn'; +import Wrapper from '@/components/ui/Wrapper'; +import { CoreBridge, PlaceholderBridge, RichText, TenTapStartKit, Toolbar, useEditorBridge, useEditorContent } from '@10play/tentap-editor'; +import { useRouter } from 'expo-router'; +import React, { useEffect, useState } from 'react'; +import { Keyboard, View } from 'react-native'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import { useRoute } from '@react-navigation/native'; +import { StorageAccessFramework } from 'expo-file-system'; +import { useFileSystem } from '@/hooks/useFilesystem'; + +export default function EditorScreen() { + const [entryText, setEntryText] = useState(''); + const [stats, setStats] = useState<{ words: number; characters: number }>({ + words: 0, + characters: 0, + }); + + useEffect(() => { + const words = entryText.trim().split(/\s+/).filter(Boolean).length; + const characters = entryText.length; + setStats({ words, characters }); + } + , [entryText]); + + const navigation = useRouter(); + const route = useRoute(); + const { writeFile, deleteFile } = useFileSystem(); + const { fileUri, fileDatetime } = route.params as { fileUri: string | undefined, fileDatetime: Date } || { fileUri: undefined, fileDatetime: new Date() }; + const [loadedContents, setLoadedContents] = useState(""); + const [menuVisible, setMenuVisible] = useState(false); + const [dialogVisible, setDialogVisible] = useState(false); + const openMenu = () => { setMenuVisible(true); }; + const closeMenu = () => { setMenuVisible(false); }; + const openDialog = () => { setDialogVisible(true); }; + const closeDialog = () => { setDialogVisible(false); }; + + + const loadFileContent = async (fileUri: string, forceReload = false) => { + try { + // Replace with your actual file reading logic + const content = await StorageAccessFramework.readAsStringAsync(fileUri); + setLoadedContents(await marked.parse(content)); + return (await marked.parse(content)); + } catch (error) { + console.error('Error loading file content:', error); + } + }; + + const saveFileContent = async () => { + console.log("Begin Save"); + // try{ + + // } + // catch (error) { + // console.error("Error creating file:", error); + // return; + // } + try { + if (!content.html) return; + + // Convert HTML back to markdown + const turndownService = new turndown({ + headingStyle: 'atx', + hr: '---', + bulletListMarker: '-', + codeBlockStyle: 'fenced', + fence: '```', + emDelimiter: '*', + strongDelimiter: '**', + linkStyle: 'inlined', + linkReferenceStyle: 'full', + }); + + // Optional: Add custom rules for better conversion + turndownService.addRule('strikethrough', { + filter: ['del', 's'], + replacement: (content: string) => `~~${content}~~` + }); + + turndownService.addRule('underline', { + filter: ['u', 'ins'], + replacement: (content: string) => `${content}` + }); + const markdownContent = turndownService.turndown(content.html); + console.log(markdownContent); + + // Save the markdown content + await writeFile(fileUri ?? format(fileDatetime, 't'), markdownContent); + console.log(`${fileDatetime} saved successfully`); + + } catch (error) { + console.error(`Error saving ${fileUri}:`, error); + } + }; + useEffect(() => { + if (fileUri) { + loadFileContent(fileUri); + } + }, [fileUri]); + useEffect(() => { + editor.setContent(loadedContents); + }, [loadedContents]); + + const theme = useTheme(); + const entryDate = format(fileDatetime, 'LLL d, h:mm aaa'); + + const insets = useSafeAreaInsets(); + const [keyboardHeight, setKeyboardHeight] = useState(0); + useEffect(() => { + const keyboardDidShowListener = Keyboard.addListener('keyboardDidShow', () => { + setKeyboardHeight(Keyboard.metrics()?.height || 0); + }); + const keyboardDidHideListener = Keyboard.addListener('keyboardDidHide', () => { + setKeyboardHeight(0); + }); + + return () => { + keyboardDidShowListener.remove(); + keyboardDidHideListener.remove(); + }; + }, []); + const editor = useEditorBridge({ + autofocus: true, + avoidIosKeyboard: true, + theme: { + webviewContainer: { + paddingLeft: 15, + backgroundColor: theme.colors.surface, + }, + }, + bridgeExtensions: [ + ...TenTapStartKit, + CoreBridge.configureCSS(` + * { + font-size: 14px; + } + p { + margin: 0; padding: 0; + } + `), + PlaceholderBridge.configureExtension({ + placeholder: '', + }), + ], + initialContent: loadedContents, + }); + const content = { + html: useEditorContent(editor, { type: 'html' }), + text: useEditorContent(editor, { type: 'text' }), + }; + useEffect(() => { + content.text && setEntryText(content.text ?? 'Nothing loaded.'); + }, [content]); + return ( + + + { + + content.text && content.text != loadedContents ? + { + saveFileContent(); + navigation.back(); + }} /> + : + { + navigation.back(); + }} /> + + } + + { }} /> + { }} /> + { + openMenu(); + }} />}> + { + navigation.back(); + closeMenu(); + }} title="Cancel" /> + { + openDialog(); + closeMenu(); + }} title="Delete" /> + + + + + Delete file? + + Entry for + {' '} + + {format(fileDatetime, "MMM dd, yyyy, HH:mm:ss")} will be permanently deleted. + + + + + + + + {/* */} + + + + {/* */} + + + ); +}; \ No newline at end of file diff --git a/app/HomeScreen.tsx b/app/HomeScreen.tsx index 47a4eba..44f37a7 100644 --- a/app/HomeScreen.tsx +++ b/app/HomeScreen.tsx @@ -1,11 +1,11 @@ import { Image } from 'expo-image'; -import { Platform, RefreshControl, ScrollView, StyleSheet, useColorScheme, View, ViewToken } from 'react-native'; +import { Platform, Pressable, RefreshControl, ScrollView, StyleSheet, TouchableOpacity, useColorScheme, View, ViewToken } from 'react-native'; import { HelloWave } from '@/components/HelloWave'; import ParallaxScrollView from '@/components/ParallaxScrollView'; import { ThemedText } from '@/components/ThemedText'; import { ThemedView } from '@/components/ThemedView'; -import { Card, FAB, IconButton, List, Surface, Text, useTheme } from 'react-native-paper'; +import { ActivityIndicator, Card, FAB, IconButton, List, Surface, Text, TouchableRipple, useTheme } from 'react-native-paper'; import { useCallback, useEffect, useState } from 'react'; import AsyncStorage from '@react-native-async-storage/async-storage'; import { DATA_DIRECTORY_URI_KEY } from '@/constants/Settings'; @@ -15,19 +15,12 @@ import { useFileSystem, prettyName } from '@/hooks/useFilesystem'; import { format } from 'date-fns'; import { StorageAccessFramework } from 'expo-file-system'; import { FlatList } from 'react-native-gesture-handler'; +import Markdown from 'react-native-markdown-display'; + export default function HomeScreen() { const navigation = useNavigation(); const theme = useTheme(); - const { dataDirectoryUri, loadFiles, files, directorySize } = useFileSystem(); - const [refreshing, setRefreshing] = useState(false); - - const onRefresh = async () => { - setRefreshing(true); - await loadFiles(); - setRefreshing(false); - }; - const styles = StyleSheet.create({ fab: { position: 'absolute', @@ -58,10 +51,26 @@ export default function HomeScreen() { alignItems: 'center', }, }); + const { dataDirectoryUri, loadFiles, files, directorySize } = useFileSystem(); + const [refreshing, setRefreshing] = useState(false); const [loadedContents, setLoadedContents] = useState>(new Map()); + const [lastViewableItems, setLastViewableItems] = useState([]); - const loadFileContent = async (fileUri: string) => { - if (loadedContents.has(fileUri)) return; // Already loaded + const onRefresh = async () => { + setRefreshing(true); + setLoadedContents(new Map()); // Clear cached content + await loadFiles(); + + // Reload content for currently visible items + lastViewableItems.forEach(({ item }) => { + loadFileContent(item.uri, true); // Force reload + }); + setRefreshing(false); + }; + + + const loadFileContent = async (fileUri: string, forceReload = false) => { + if (loadedContents.has(fileUri) && !forceReload) return; // Already loaded try { // Replace with your actual file reading logic @@ -73,17 +82,21 @@ export default function HomeScreen() { }; const onViewableItemsChanged = useCallback(({ viewableItems }: { viewableItems: ViewToken[] }) => { + setLastViewableItems(viewableItems); // Track visible items viewableItems.forEach(({ item }) => { loadFileContent(item.uri); }); }, []); - const viewabilityConfig = { - itemVisiblePercentThreshold: 50, // Load when 50% visible - }; - const renderFileItem = ({ item: f }: { item: typeof files[0] }) => ( - + { + navigation.navigate('Editor', { + fileUri: f.uri, fileDatetime: f.datetime + }, + ); + }} + > - + {format(f.datetime, "hh:mm aaa")} {loadedContents.has(f.uri) && ( - - {loadedContents.get(f.uri)} - + + {loadedContents.get(f.uri) || + + } + )} - + ); return ( @@ -112,7 +161,7 @@ export default function HomeScreen() { renderItem={renderFileItem} keyExtractor={(item) => item.uri} onViewableItemsChanged={onViewableItemsChanged} - viewabilityConfig={viewabilityConfig} + viewabilityConfig={{ itemVisiblePercentThreshold: 50 }} refreshControl={ @@ -138,7 +187,7 @@ export default function HomeScreen() { } /> { - navigation.navigate('NewEntry' as never); + navigation.navigate('Editor' as never); }} /> diff --git a/app/NewEntry.tsx b/app/NewEntry.tsx deleted file mode 100644 index d7f9eb2..0000000 --- a/app/NewEntry.tsx +++ /dev/null @@ -1,276 +0,0 @@ -// import EditorMenu from '@/components/EditorMenu'; -import { format } from 'date-fns'; -// import { useRouter } from 'expo-router'; -// import React, { useEffect, useState } from 'react'; -// import { Keyboard, ScrollView, StyleSheet, TextInput } from 'react-native'; -import { Appbar, useTheme } from 'react-native-paper'; -// import { useSafeAreaInsets } from 'react-native-safe-area-context'; -// import { EditorContent, useEditor } from 'rn-text-editor'; - -// export default function NewEntryScreen() { -// const navigation = useRouter(); -// const theme = useTheme(); -// const entryDate = format(new Date(), 'LLL d, h:mm aaa'); - - -// const editor = useEditor({ -// enableCoreExtensions: true, -// onUpdate(props) { -// const newText = props.editor.getNativeText(); -// // setEntryText(newText); -// }, -// }); - - - -// return ( -// -// -// { -// stats['characters'] === 0 ? -// { -// navigation.back(); -// }} /> -// : -// { -// navigation.back(); -// }} /> -// } -// -// { }} /> -// { }} /> -// -// -// {stats.words} words ยท {stats.characters} characters -// -// -// {/* -// */} -// -// - -// {/* -// < IconButton -// icon="format-bold" -// size={24} accessible={false} - -// onPress={() => { -// console.log('Bold button pressed'); inputRef.current?.focus(); - -// }} -// /> -// { -// console.log('Italic button pressed'); -// }} -// /> -// { -// console.log('Underline button pressed'); -// }} -// /> -// */} -// -// -// ); -// } -// const styles = StyleSheet.create({ -// container: { -// flex: 1, -// justifyContent: 'center', -// paddingHorizontal: 1, -// }, -// editorContainer: { -// paddingHorizontal: 5, -// flex: 1 -// }, -// box: { -// width: 60, -// height: 60, -// marginVertical: 20, -// }, -// }); - -import Wrapper from '@/components/ui/Wrapper'; -import { CoreBridge, RichText, TenTapStartKit, Toolbar, useEditorBridge, useEditorContent } from '@10play/tentap-editor'; -import { useRouter } from 'expo-router'; -import React, { useEffect, useState } from 'react'; -import { Keyboard, View } from 'react-native'; -import { useSafeAreaInsets } from 'react-native-safe-area-context'; - -export default function NewEntryScreen() { - const [entryText, setEntryText] = useState(''); - const [stats, setStats] = useState<{ words: number; characters: number }>({ - words: 0, - characters: 0, - }); - - useEffect(() => { - const words = entryText.trim().split(/\s+/).filter(Boolean).length; - const characters = entryText.length; - setStats({ words, characters }); - console.log('Entry text updated:', entryText); - } - , [entryText]); - - const navigation = useRouter(); - const theme = useTheme(); - const entryDate = format(new Date(), 'LLL d, h:mm aaa'); - - const insets = useSafeAreaInsets(); - const [keyboardHeight, setKeyboardHeight] = useState(0); - useEffect(() => { - const keyboardDidShowListener = Keyboard.addListener('keyboardDidShow', () => { - setKeyboardHeight(Keyboard.metrics()?.height || 0); - }); - const keyboardDidHideListener = Keyboard.addListener('keyboardDidHide', () => { - setKeyboardHeight(0); - }); - - return () => { - keyboardDidShowListener.remove(); - keyboardDidHideListener.remove(); - }; - }, []); - - const editor = useEditorBridge({ - autofocus: true, - avoidIosKeyboard: true, - theme: { - webviewContainer: { - paddingLeft: 15, - backgroundColor: theme.colors.surface, - }, - }, - bridgeExtensions: [ - ...TenTapStartKit, - CoreBridge.configureCSS(` - * { - font-size: 12px; - } - `), // Custom font - ], - }); - const content = { - html: useEditorContent(editor, { type: 'html' }), - text: useEditorContent(editor, { type: 'text' }), - }; - useEffect(() => { - content && setEntryText(content.text ?? ''); - }, [content]); - - return ( - - - { - stats['characters'] === 0 ? - { - navigation.back(); - }} /> - : - { - navigation.back(); - }} /> - } - - { }} /> - { }} /> - - - {/* */} - - - - {/* */} - - - ); -}; \ No newline at end of file diff --git a/app/_layout.tsx b/app/_layout.tsx index ffbba32..b214b87 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -14,7 +14,7 @@ import { IconSymbol } from '@/components/ui/IconSymbol'; import { TouchableOpacity, View } from 'react-native'; import NullComponent from '@/components/NullComponent'; import { useEffect } from 'react'; -import NewEntryScreen from './NewEntry'; +import EditorScreen from './Editor'; import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'; // Import your tab screens directly @@ -226,8 +226,8 @@ export default function RootLayout() { })} /> { + const writeFile = useCallback(async (fileUriOrDatetime: string, content: string) => { if (!dataDirectoryUri) throw new Error('No directory selected'); - - const fileUri = `${dataDirectoryUri}/${filename}`; + const fileUriDatetime = new Date(parseInt(fileUriOrDatetime) * 1000); + let fileUri = fileUriOrDatetime; + if (parseInt(fileUriOrDatetime)) { // If it's a datetime in seconds + const filename = format(fileUriDatetime, 'yyyy-MM-dd-HH-mm-ss'); + fileUri = await StorageAccessFramework.createFileAsync( + dataDirectoryUri, + `${filename}.md`, + 'text/markdown' + ); + } + // const fileUri = `${dataDirectoryUri}/${filename}`; await FileSystem.writeAsStringAsync(fileUri, content); await loadFiles(); // Refresh file list }, [dataDirectoryUri, loadFiles]); @@ -138,6 +152,11 @@ export function useFileSystem() { return await FileSystem.readAsStringAsync(fileUri); }, [dataDirectoryUri]); + const deleteFile = useCallback(async (filename: string): Promise => { + return await FileSystem.deleteAsync(filename, { idempotent: true }); + } + , []); + const saveDataDirectoryUri = useCallback(async (uri: string | null) => { try { if (uri) { @@ -164,6 +183,7 @@ export function useFileSystem() { files, writeFile, readFile, + deleteFile, loadFiles, hasDirectory: !!dataDirectoryUri, directorySize, diff --git a/package-lock.json b/package-lock.json index 6132adf..2646ca2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,7 @@ "@react-navigation/drawer": "^7.4.2", "@react-navigation/elements": "^2.3.8", "@react-navigation/native": "^7.1.6", + "@types/turndown": "^5.0.5", "date-fns": "^4.1.0", "expo": "~53.0.11", "expo-blur": "~14.1.5", @@ -42,6 +43,7 @@ "react-native": "0.79.3", "react-native-edge-to-edge": "1.6.0", "react-native-gesture-handler": "~2.24.0", + "react-native-markdown-display": "^7.0.2", "react-native-paper": "^5.14.5", "react-native-pell-rich-editor": "^1.9.0", "react-native-reanimated": "~3.17.4", @@ -51,7 +53,7 @@ "react-native-web": "~0.20.0", "react-native-webview": "13.13.5", "rn-text-editor": "^0.2.0", - "turndown": "^7.2.0" + "turndown-rn": "^6.1.0" }, "devDependencies": { "@babel/core": "^7.25.2", @@ -3032,12 +3034,6 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@mixmark-io/domino": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@mixmark-io/domino/-/domino-2.2.0.tgz", - "integrity": "sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw==", - "license": "BSD-2-Clause" - }, "node_modules/@napi-rs/wasm-runtime": { "version": "0.2.11", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.11.tgz", @@ -6281,6 +6277,12 @@ "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", "license": "MIT" }, + "node_modules/@types/turndown": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/@types/turndown/-/turndown-5.0.5.tgz", + "integrity": "sha512-TL2IgGgc7B5j78rIccBtlYAnkuv8nUQqhQc+DSYV5j9Be9XOcm/SKOVRuA47xAVI3680Tk9B1d8flK2GWT2+4w==", + "license": "MIT" + }, "node_modules/@types/use-sync-external-store": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", @@ -7916,6 +7918,15 @@ "node": ">=6" } }, + "node_modules/camelize": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz", + "integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001723", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001723.tgz", @@ -8288,6 +8299,12 @@ "url": "https://opencollective.com/core-js" } }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, "node_modules/cosmiconfig": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.2.1.tgz", @@ -8385,6 +8402,15 @@ "node": ">=8" } }, + "node_modules/css-color-keywords": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", + "integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==", + "license": "ISC", + "engines": { + "node": ">=4" + } + }, "node_modules/css-in-js-utils": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/css-in-js-utils/-/css-in-js-utils-3.1.0.tgz", @@ -8394,6 +8420,17 @@ "hyphenate-style-name": "^1.0.3" } }, + "node_modules/css-to-react-native": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz", + "integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==", + "license": "MIT", + "dependencies": { + "camelize": "^1.0.0", + "css-color-keywords": "^1.0.0", + "postcss-value-parser": "^4.0.2" + } + }, "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", @@ -8670,6 +8707,62 @@ "node": ">=0.10.0" } }, + "node_modules/dom-serializer": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.2.tgz", + "integrity": "sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.0.1", + "entities": "^2.0.0" + } + }, + "node_modules/dom-serializer/node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/dom-serializer/node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "license": "BSD-2-Clause", + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", + "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==", + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.2.tgz", + "integrity": "sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "1" + } + }, + "node_modules/domutils": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz", + "integrity": "sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "0", + "domelementtype": "1" + } + }, "node_modules/dotenv": { "version": "16.4.7", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", @@ -9464,6 +9557,12 @@ "node": ">=6" } }, + "node_modules/eventemitter2": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-1.0.5.tgz", + "integrity": "sha512-EUFhWUYzqqBZlzBMI+dPU8rnKXfQZEUnitnccQuEIAnvWFHCpt3+4fts2+4dpxLtlsiseVXCMFg37KjYChSxpg==", + "license": "MIT" + }, "node_modules/exec-async": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/exec-async/-/exec-async-2.2.0.tgz", @@ -10767,6 +10866,63 @@ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "license": "ISC" }, + "node_modules/htmlparser2-without-node-native": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/htmlparser2-without-node-native/-/htmlparser2-without-node-native-3.9.2.tgz", + "integrity": "sha512-+FplQXqmY5fRx6vCIp2P5urWaoBCpTNJMXnKP/6mNCcyb+AZWWJzA8D03peXfozlxDL+vpgLK5dJblqEgu8j6A==", + "license": "MIT", + "dependencies": { + "domelementtype": "^1.3.0", + "domhandler": "^2.3.0", + "domutils": "^1.5.1", + "entities": "^1.1.1", + "eventemitter2": "^1.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.0.2" + } + }, + "node_modules/htmlparser2-without-node-native/node_modules/entities": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz", + "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==", + "license": "BSD-2-Clause" + }, + "node_modules/htmlparser2-without-node-native/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/htmlparser2-without-node-native/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/htmlparser2-without-node-native/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/htmlparser2-without-node-native/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/http-cache-semantics": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", @@ -11790,6 +11946,16 @@ "integrity": "sha512-0wM3YBWtYePOjfyXQH5MWQ8H7sdk5EXSwZvmSLKk2RboVQ2Bu239jycHDz5J/8Blf3K0Qnoy2b6xD+z10MFB+Q==", "license": "0BSD" }, + "node_modules/jsdom-jscore-rn": { + "version": "0.1.9", + "resolved": "https://registry.npmjs.org/jsdom-jscore-rn/-/jsdom-jscore-rn-0.1.9.tgz", + "integrity": "sha512-9dJqG4LtSDwZl29U2+vdmhsZW/yVng8cPODR43pxcqB4pWia81VzfWTr5oNOUIhHGxT214T5eAg5z1xjdx2osQ==", + "license": "MIT", + "dependencies": { + "htmlparser2-without-node-native": "^3.9.2", + "querystring": "^0.2.0" + } + }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -13966,6 +14132,12 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, "node_modules/progress": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", @@ -14289,6 +14461,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/querystring": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.1.tgz", + "integrity": "sha512-wkvS7mL/JMugcup3/rMitHmd9ecIGd2lhFhK9N3UUQ450h66d1r3Y9nvXzQAW1Lq+wyx61k/1pfKS5KuKiyEbg==", + "deprecated": "The querystring API is considered Legacy. new code should use the URLSearchParams API instead.", + "license": "MIT", + "engines": { + "node": ">=0.4.x" + } + }, "node_modules/queue": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz", @@ -14540,6 +14722,15 @@ "react-native": "*" } }, + "node_modules/react-native-fit-image": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/react-native-fit-image/-/react-native-fit-image-1.5.5.tgz", + "integrity": "sha512-Wl3Vq2DQzxgsWKuW4USfck9zS7YzhvLNPpkwUUCF90bL32e1a0zOVQ3WsJILJOwzmPdHfzZmWasiiAUNBkhNkg==", + "license": "Beerware", + "dependencies": { + "prop-types": "^15.5.10" + } + }, "node_modules/react-native-gesture-handler": { "version": "2.24.0", "resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-2.24.0.tgz", @@ -14565,6 +14756,74 @@ "react-native": "*" } }, + "node_modules/react-native-markdown-display": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/react-native-markdown-display/-/react-native-markdown-display-7.0.2.tgz", + "integrity": "sha512-Mn4wotMvMfLAwbX/huMLt202W5DsdpMO/kblk+6eUs55S57VVNni1gzZCh5qpznYLjIQELNh50VIozEfY6fvaQ==", + "license": "MIT", + "dependencies": { + "css-to-react-native": "^3.0.0", + "markdown-it": "^10.0.0", + "prop-types": "^15.7.2", + "react-native-fit-image": "^1.5.5" + }, + "peerDependencies": { + "react": ">=16.2.0", + "react-native": ">=0.50.4" + } + }, + "node_modules/react-native-markdown-display/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/react-native-markdown-display/node_modules/entities": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.0.3.tgz", + "integrity": "sha512-MyoZ0jgnLvB2X3Lg5HqpFmn1kybDiIfEQmKzTb5apr51Rb+T3KdmMiqa70T+bhGnyv7bQ6WMj2QMHpGMmlrUYQ==", + "license": "BSD-2-Clause" + }, + "node_modules/react-native-markdown-display/node_modules/linkify-it": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-2.2.0.tgz", + "integrity": "sha512-GnAl/knGn+i1U/wjBz3akz2stz+HrHLsxMwHQGofCDfPvlf+gDKN58UtfmUquTY4/MXeE2x7k19KQmeoZi94Iw==", + "license": "MIT", + "dependencies": { + "uc.micro": "^1.0.1" + } + }, + "node_modules/react-native-markdown-display/node_modules/markdown-it": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-10.0.0.tgz", + "integrity": "sha512-YWOP1j7UbDNz+TumYP1kpwnP0aEa711cJjrAQrzd0UXlbJfc5aAq0F/PZHjiioqDC1NKgvIMX+o+9Bk7yuM2dg==", + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "entities": "~2.0.0", + "linkify-it": "^2.0.0", + "mdurl": "^1.0.1", + "uc.micro": "^1.0.5" + }, + "bin": { + "markdown-it": "bin/markdown-it.js" + } + }, + "node_modules/react-native-markdown-display/node_modules/mdurl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", + "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==", + "license": "MIT" + }, + "node_modules/react-native-markdown-display/node_modules/uc.micro": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", + "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==", + "license": "MIT" + }, "node_modules/react-native-paper": { "version": "5.14.5", "resolved": "https://registry.npmjs.org/react-native-paper/-/react-native-paper-5.14.5.tgz", @@ -16665,13 +16924,13 @@ "license": "0BSD", "optional": true }, - "node_modules/turndown": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/turndown/-/turndown-7.2.0.tgz", - "integrity": "sha512-eCZGBN4nNNqM9Owkv9HAtWRYfLA4h909E/WGAWWBpmB275ehNhZyk87/Tpvjbp0jjNl9XwCsbe6bm6CqFsgD+A==", + "node_modules/turndown-rn": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/turndown-rn/-/turndown-rn-6.1.0.tgz", + "integrity": "sha512-wyxm5XUNRFrg8qOnZStkDKe3nAjxtYjCvZnwVHI+D66kIq6WVjyzO+VbgKEmy7ajkP7bTGDIGG1EbOZpCNfYvA==", "license": "MIT", "dependencies": { - "@mixmark-io/domino": "^2.2.0" + "jsdom-jscore-rn": "^0.1.8" } }, "node_modules/type-check": { diff --git a/package.json b/package.json index e15e783..3d4e999 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "@react-navigation/drawer": "^7.4.2", "@react-navigation/elements": "^2.3.8", "@react-navigation/native": "^7.1.6", + "@types/turndown": "^5.0.5", "date-fns": "^4.1.0", "expo": "~53.0.11", "expo-blur": "~14.1.5", @@ -45,6 +46,7 @@ "react-native": "0.79.3", "react-native-edge-to-edge": "1.6.0", "react-native-gesture-handler": "~2.24.0", + "react-native-markdown-display": "^7.0.2", "react-native-paper": "^5.14.5", "react-native-pell-rich-editor": "^1.9.0", "react-native-reanimated": "~3.17.4", @@ -54,7 +56,7 @@ "react-native-web": "~0.20.0", "react-native-webview": "13.13.5", "rn-text-editor": "^0.2.0", - "turndown": "^7.2.0" + "turndown-rn": "^6.1.0" }, "devDependencies": { "@babel/core": "^7.25.2",