diff --git a/apps/ios-example/EnrichedMarkdownExample/EnrichedMarkdownExample.xcodeproj/project.pbxproj b/apps/ios-example/EnrichedMarkdownExample/EnrichedMarkdownExample.xcodeproj/project.pbxproj index d31f15c57..3a67dcdc1 100644 --- a/apps/ios-example/EnrichedMarkdownExample/EnrichedMarkdownExample.xcodeproj/project.pbxproj +++ b/apps/ios-example/EnrichedMarkdownExample/EnrichedMarkdownExample.xcodeproj/project.pbxproj @@ -8,16 +8,50 @@ /* Begin PBXBuildFile section */ A10000010000000000000001 /* EnrichedMarkdownExampleApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = A10000020000000000000001 /* EnrichedMarkdownExampleApp.swift */; }; - A10000010000000000000002 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A10000020000000000000002 /* ContentView.swift */; }; A10000010000000000000003 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A10000020000000000000003 /* Assets.xcassets */; }; A10000010000000000000004 /* EnrichedMarkdown in Frameworks */ = {isa = PBXBuildFile; productRef = A10000050000000000000001 /* EnrichedMarkdown */; }; + A10000010000000000000010 /* AppShell.swift in Sources */ = {isa = PBXBuildFile; fileRef = A10000020000000000000010 /* AppShell.swift */; }; + A10000010000000000000011 /* ExampleRoute.swift in Sources */ = {isa = PBXBuildFile; fileRef = A10000020000000000000011 /* ExampleRoute.swift */; }; + A10000010000000000000012 /* HomeScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = A10000020000000000000012 /* HomeScreen.swift */; }; + A10000010000000000000013 /* HomeScreenButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = A10000020000000000000013 /* HomeScreenButton.swift */; }; + A10000010000000000000014 /* TextScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = A10000020000000000000014 /* TextScreen.swift */; }; + A10000010000000000000015 /* PlaygroundScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = A10000020000000000000015 /* PlaygroundScreen.swift */; }; + A10000010000000000000016 /* ExampleMarkdownStyles.swift in Sources */ = {isa = PBXBuildFile; fileRef = A10000020000000000000016 /* ExampleMarkdownStyles.swift */; }; + A10000010000000000000017 /* ExampleFontRegistrar.swift in Sources */ = {isa = PBXBuildFile; fileRef = A10000020000000000000017 /* ExampleFontRegistrar.swift */; }; + A10000010000000000000020 /* sample_markdown.md in Resources */ = {isa = PBXBuildFile; fileRef = A10000020000000000000020 /* sample_markdown.md */; }; + A10000010000000000000021 /* logo.png in Resources */ = {isa = PBXBuildFile; fileRef = A10000020000000000000021 /* logo.png */; }; + A10000010000000000000022 /* logo_icon.png in Resources */ = {isa = PBXBuildFile; fileRef = A10000020000000000000022 /* logo_icon.png */; }; + A10000010000000000000030 /* Montserrat-Regular.ttf in Resources */ = {isa = PBXBuildFile; fileRef = A10000020000000000000030 /* Montserrat-Regular.ttf */; }; + A10000010000000000000031 /* Montserrat-Bold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = A10000020000000000000031 /* Montserrat-Bold.ttf */; }; + A10000010000000000000032 /* Montserrat-SemiBold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = A10000020000000000000032 /* Montserrat-SemiBold.ttf */; }; + A10000010000000000000033 /* Montserrat-Medium.ttf in Resources */ = {isa = PBXBuildFile; fileRef = A10000020000000000000033 /* Montserrat-Medium.ttf */; }; + A10000010000000000000034 /* Montserrat-Italic.ttf in Resources */ = {isa = PBXBuildFile; fileRef = A10000020000000000000034 /* Montserrat-Italic.ttf */; }; + A10000010000000000000036 /* Montserrat-BoldItalic.ttf in Resources */ = {isa = PBXBuildFile; fileRef = A10000020000000000000036 /* Montserrat-BoldItalic.ttf */; }; + A10000010000000000000035 /* CourierPrime-Regular.ttf in Resources */ = {isa = PBXBuildFile; fileRef = A10000020000000000000035 /* CourierPrime-Regular.ttf */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ A10000020000000000000001 /* EnrichedMarkdownExampleApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnrichedMarkdownExampleApp.swift; sourceTree = ""; }; - A10000020000000000000002 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; A10000020000000000000003 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; A10000020000000000000004 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + A10000020000000000000010 /* AppShell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppShell.swift; sourceTree = ""; }; + A10000020000000000000011 /* ExampleRoute.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleRoute.swift; sourceTree = ""; }; + A10000020000000000000012 /* HomeScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreen.swift; sourceTree = ""; }; + A10000020000000000000013 /* HomeScreenButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenButton.swift; sourceTree = ""; }; + A10000020000000000000014 /* TextScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextScreen.swift; sourceTree = ""; }; + A10000020000000000000015 /* PlaygroundScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaygroundScreen.swift; sourceTree = ""; }; + A10000020000000000000016 /* ExampleMarkdownStyles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleMarkdownStyles.swift; sourceTree = ""; }; + A10000020000000000000017 /* ExampleFontRegistrar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleFontRegistrar.swift; sourceTree = ""; }; + A10000020000000000000020 /* sample_markdown.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = sample_markdown.md; sourceTree = ""; }; + A10000020000000000000021 /* logo.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = logo.png; sourceTree = ""; }; + A10000020000000000000022 /* logo_icon.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = logo_icon.png; sourceTree = ""; }; + A10000020000000000000030 /* Montserrat-Regular.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Montserrat-Regular.ttf"; sourceTree = ""; }; + A10000020000000000000031 /* Montserrat-Bold.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Montserrat-Bold.ttf"; sourceTree = ""; }; + A10000020000000000000032 /* Montserrat-SemiBold.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Montserrat-SemiBold.ttf"; sourceTree = ""; }; + A10000020000000000000033 /* Montserrat-Medium.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Montserrat-Medium.ttf"; sourceTree = ""; }; + A10000020000000000000034 /* Montserrat-Italic.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Montserrat-Italic.ttf"; sourceTree = ""; }; + A10000020000000000000036 /* Montserrat-BoldItalic.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Montserrat-BoldItalic.ttf"; sourceTree = ""; }; + A10000020000000000000035 /* CourierPrime-Regular.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "CourierPrime-Regular.ttf"; sourceTree = ""; }; A10000030000000000000001 /* EnrichedMarkdownExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = EnrichedMarkdownExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ @@ -45,7 +79,15 @@ isa = PBXGroup; children = ( A10000020000000000000001 /* EnrichedMarkdownExampleApp.swift */, - A10000020000000000000002 /* ContentView.swift */, + A10000020000000000000010 /* AppShell.swift */, + A10000020000000000000011 /* ExampleRoute.swift */, + A10000020000000000000012 /* HomeScreen.swift */, + A10000020000000000000013 /* HomeScreenButton.swift */, + A10000020000000000000014 /* TextScreen.swift */, + A10000020000000000000015 /* PlaygroundScreen.swift */, + A10000020000000000000016 /* ExampleMarkdownStyles.swift */, + A10000020000000000000017 /* ExampleFontRegistrar.swift */, + A10000060000000000000004 /* Resources */, A10000020000000000000003 /* Assets.xcassets */, A10000020000000000000004 /* Info.plist */, ); @@ -60,6 +102,31 @@ name = Products; sourceTree = ""; }; + A10000060000000000000004 /* Resources */ = { + isa = PBXGroup; + children = ( + A10000060000000000000005 /* Fonts */, + A10000020000000000000020 /* sample_markdown.md */, + A10000020000000000000021 /* logo.png */, + A10000020000000000000022 /* logo_icon.png */, + ); + path = Resources; + sourceTree = ""; + }; + A10000060000000000000005 /* Fonts */ = { + isa = PBXGroup; + children = ( + A10000020000000000000030 /* Montserrat-Regular.ttf */, + A10000020000000000000031 /* Montserrat-Bold.ttf */, + A10000020000000000000032 /* Montserrat-SemiBold.ttf */, + A10000020000000000000033 /* Montserrat-Medium.ttf */, + A10000020000000000000034 /* Montserrat-Italic.ttf */, + A10000020000000000000036 /* Montserrat-BoldItalic.ttf */, + A10000020000000000000035 /* CourierPrime-Regular.ttf */, + ); + path = Fonts; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -125,6 +192,16 @@ buildActionMask = 2147483647; files = ( A10000010000000000000003 /* Assets.xcassets in Resources */, + A10000010000000000000020 /* sample_markdown.md in Resources */, + A10000010000000000000021 /* logo.png in Resources */, + A10000010000000000000022 /* logo_icon.png in Resources */, + A10000010000000000000030 /* Montserrat-Regular.ttf in Resources */, + A10000010000000000000031 /* Montserrat-Bold.ttf in Resources */, + A10000010000000000000032 /* Montserrat-SemiBold.ttf in Resources */, + A10000010000000000000033 /* Montserrat-Medium.ttf in Resources */, + A10000010000000000000034 /* Montserrat-Italic.ttf in Resources */, + A10000010000000000000036 /* Montserrat-BoldItalic.ttf in Resources */, + A10000010000000000000035 /* CourierPrime-Regular.ttf in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -136,7 +213,14 @@ buildActionMask = 2147483647; files = ( A10000010000000000000001 /* EnrichedMarkdownExampleApp.swift in Sources */, - A10000010000000000000002 /* ContentView.swift in Sources */, + A10000010000000000000010 /* AppShell.swift in Sources */, + A10000010000000000000011 /* ExampleRoute.swift in Sources */, + A10000010000000000000012 /* HomeScreen.swift in Sources */, + A10000010000000000000013 /* HomeScreenButton.swift in Sources */, + A10000010000000000000014 /* TextScreen.swift in Sources */, + A10000010000000000000015 /* PlaygroundScreen.swift in Sources */, + A10000010000000000000016 /* ExampleMarkdownStyles.swift in Sources */, + A10000010000000000000017 /* ExampleFontRegistrar.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -161,7 +245,7 @@ "DEBUG=1", "$(inherited)", ); - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -182,7 +266,7 @@ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SWIFT_COMPILATION_MODE = wholemodule; diff --git a/apps/ios-example/EnrichedMarkdownExample/EnrichedMarkdownExample/AppShell.swift b/apps/ios-example/EnrichedMarkdownExample/EnrichedMarkdownExample/AppShell.swift new file mode 100644 index 000000000..133c82251 --- /dev/null +++ b/apps/ios-example/EnrichedMarkdownExample/EnrichedMarkdownExample/AppShell.swift @@ -0,0 +1,108 @@ +import EnrichedMarkdown +import SwiftUI + +struct AppShell: View { + @State private var route: ExampleRoute = .home + @State private var unavailableRouteName: String? + @State private var sampleMarkdown = "" + + private var navigationTitle: String { + switch route { + case .home: + return "Enriched Markdown Examples" + case .playground: + return "Playground" + case .text: + return "Text" + case .input, .stream, .storybook: + return routeName(route) + } + } + + var body: some View { + NavigationStack { + Group { + switch route { + case .home: + HomeScreen(onNavigate: handleNavigate) + case .playground: + PlaygroundScreen() + case .text: + TextScreen(markdown: sampleMarkdown) + case .input, .stream, .storybook: + EmptyView() + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .navigationTitle(navigationTitle) + .navigationBarTitleDisplayMode(.inline) + .toolbarBackground(AppColors.toolbarBackground, for: .navigationBar) + .toolbarBackground(.visible, for: .navigationBar) + .toolbarColorScheme(.light, for: .navigationBar) + .toolbar { + if route != .home { + ToolbarItem(placement: .navigationBarLeading) { + Button("Back") { + route = .home + } + .foregroundStyle(AppColors.toolbarForeground) + } + } + } + } + .tint(AppColors.toolbarForeground) + .onAppear { + if sampleMarkdown.isEmpty { + sampleMarkdown = BundleLoader.sampleMarkdown + } + } + .alert( + unavailableRouteName.map { "\($0) is not available on iOS yet" } ?? "", + isPresented: Binding( + get: { unavailableRouteName != nil }, + set: { if !$0 { unavailableRouteName = nil } } + ) + ) { + Button("OK", role: .cancel) {} + } + } + + private func handleNavigate(_ target: ExampleRoute) { + switch target { + case .playground, .text: + route = target + case .input, .stream, .storybook: + unavailableRouteName = routeName(target) + case .home: + route = .home + } + } + + private func routeName(_ route: ExampleRoute) -> String { + switch route { + case .home: return "Home" + case .playground: return "Playground" + case .text: return "Text" + case .input: return "Input" + case .stream: return "Stream" + case .storybook: return "Storybook" + } + } +} + +private enum AppColors { + static let toolbarBackground = Color(red: 190 / 255, green: 235 / 255, blue: 208 / 255) + static let toolbarForeground = Color(red: 0 / 255, green: 26 / 255, blue: 114 / 255) +} + +private enum BundleLoader { + static var sampleMarkdown: String { + guard + let url = Bundle.main.url(forResource: "sample_markdown", withExtension: "md"), + let content = try? String(contentsOf: url, encoding: .utf8) + else { + return "" + } + return content + } +} diff --git a/apps/ios-example/EnrichedMarkdownExample/EnrichedMarkdownExample/ContentView.swift b/apps/ios-example/EnrichedMarkdownExample/EnrichedMarkdownExample/ContentView.swift deleted file mode 100644 index cd505357d..000000000 --- a/apps/ios-example/EnrichedMarkdownExample/EnrichedMarkdownExample/ContentView.swift +++ /dev/null @@ -1,13 +0,0 @@ -import EnrichedMarkdown -import SwiftUI - -struct ContentView: View { - var body: some View { - Text("Enriched Markdown iOS Example") - .padding() - } -} - -#Preview { - ContentView() -} diff --git a/apps/ios-example/EnrichedMarkdownExample/EnrichedMarkdownExample/EnrichedMarkdownExampleApp.swift b/apps/ios-example/EnrichedMarkdownExample/EnrichedMarkdownExample/EnrichedMarkdownExampleApp.swift index 395b828ca..d4b8485b9 100644 --- a/apps/ios-example/EnrichedMarkdownExample/EnrichedMarkdownExample/EnrichedMarkdownExampleApp.swift +++ b/apps/ios-example/EnrichedMarkdownExample/EnrichedMarkdownExample/EnrichedMarkdownExampleApp.swift @@ -2,9 +2,13 @@ import SwiftUI @main struct EnrichedMarkdownExampleApp: App { + init() { + ExampleFontRegistrar.registerBundledFonts() + } + var body: some Scene { WindowGroup { - ContentView() + AppShell() } } } diff --git a/apps/ios-example/EnrichedMarkdownExample/EnrichedMarkdownExample/ExampleFontRegistrar.swift b/apps/ios-example/EnrichedMarkdownExample/EnrichedMarkdownExample/ExampleFontRegistrar.swift new file mode 100644 index 000000000..e2800e1aa --- /dev/null +++ b/apps/ios-example/EnrichedMarkdownExample/EnrichedMarkdownExample/ExampleFontRegistrar.swift @@ -0,0 +1,54 @@ +import CoreText +import UIKit + +enum ExampleFontRegistrar { + private static let fontFilenames = [ + "Montserrat-Regular", + "Montserrat-Bold", + "Montserrat-SemiBold", + "Montserrat-Medium", + "Montserrat-Italic", + "Montserrat-BoldItalic", + "CourierPrime-Regular", + ] + + /// Registers example fonts from the app bundle before any markdown is rendered. + static func registerBundledFonts() { + var registeredCount = 0 + for filename in fontFilenames where registerFont(filename: filename) { + registeredCount += 1 + } + + #if DEBUG + if registeredCount == 0 { + assertionFailure( + """ + No bundled fonts were found in EnrichedMarkdownExample.app. + Delete the app from the simulator, Product > Clean Build Folder, then run again. + """ + ) + } else if registeredCount < fontFilenames.count { + print("ExampleFontRegistrar: registered \(registeredCount)/\(fontFilenames.count) fonts") + } + #endif + } + + @discardableResult + private static func registerFont(filename: String) -> Bool { + guard let url = fontURL(for: filename) else { return false } + + var error: Unmanaged? + let didRegister = CTFontManagerRegisterFontsForURL(url as CFURL, .process, &error) + if !didRegister, let error { + #if DEBUG + print("ExampleFontRegistrar: failed to register \(filename): \(error.takeRetainedValue())") + #endif + } + return didRegister + } + + private static func fontURL(for filename: String) -> URL? { + Bundle.main.url(forResource: filename, withExtension: "ttf", subdirectory: "Fonts") + ?? Bundle.main.url(forResource: filename, withExtension: "ttf") + } +} diff --git a/apps/ios-example/EnrichedMarkdownExample/EnrichedMarkdownExample/ExampleMarkdownStyles.swift b/apps/ios-example/EnrichedMarkdownExample/EnrichedMarkdownExample/ExampleMarkdownStyles.swift new file mode 100644 index 000000000..206b31e81 --- /dev/null +++ b/apps/ios-example/EnrichedMarkdownExample/EnrichedMarkdownExample/ExampleMarkdownStyles.swift @@ -0,0 +1,142 @@ +import EnrichedMarkdown +import SwiftUI + +private enum ExampleFonts { + static let montserratRegular = "Montserrat-Regular" + static let montserratBold = "Montserrat-Bold" + static let montserratSemiBold = "Montserrat-SemiBold" + static let montserratMedium = "Montserrat-Medium" + static let montserratItalic = "Montserrat-Italic" + static let courierPrimeRegular = "CourierPrime-Regular" +} + +/// Optional layered override with explicit hex colors matching Android `ExampleMarkdownStyles.kt`. +/// The library's `.default` theme uses semantic SwiftUI colors instead; apply this theme to +/// demonstrate cross-platform hex parity or custom branding. +let CustomMarkdownTheme = MarkdownTheme { + Paragraph() + .fontFamily(ExampleFonts.montserratRegular, size: 16) + .foregroundStyle(Color(red: 31 / 255, green: 41 / 255, blue: 55 / 255)) + .lineHeight(26) + .marginBottom(16) + + Heading(1) + .fontFamily(ExampleFonts.montserratBold, size: 30) + .foregroundStyle(Color(red: 17 / 255, green: 24 / 255, blue: 39 / 255)) + .lineHeight(38) + .marginBottom(8) + + Heading(2) + .fontFamily(ExampleFonts.montserratBold, size: 24) + .foregroundStyle(Color(red: 17 / 255, green: 24 / 255, blue: 39 / 255)) + .lineHeight(32) + .marginBottom(8) + + Heading(3) + .fontFamily(ExampleFonts.montserratSemiBold, size: 20) + .foregroundStyle(Color(red: 31 / 255, green: 41 / 255, blue: 55 / 255)) + .lineHeight(28) + .marginBottom(8) + + Heading(4) + .fontFamily(ExampleFonts.montserratSemiBold, size: 18) + .foregroundStyle(Color(red: 31 / 255, green: 41 / 255, blue: 55 / 255)) + .lineHeight(26) + .marginBottom(8) + + Heading(5) + .fontFamily(ExampleFonts.montserratMedium, size: 16) + .foregroundStyle(Color(red: 55 / 255, green: 65 / 255, blue: 81 / 255)) + .lineHeight(24) + .marginBottom(8) + + Heading(6) + .fontFamily(ExampleFonts.montserratMedium, size: 14) + .foregroundStyle(Color(red: 75 / 255, green: 85 / 255, blue: 99 / 255)) + .lineHeight(22) + .marginBottom(8) + + Blockquote() + .fontFamily(ExampleFonts.montserratItalic, size: 16) + .foregroundStyle(Color(red: 75 / 255, green: 85 / 255, blue: 99 / 255)) + .lineHeight(26) + .borderColor(Color(red: 209 / 255, green: 213 / 255, blue: 219 / 255)) + .borderWidth(3) + .backgroundStyle(Color(red: 249 / 255, green: 250 / 255, blue: 251 / 255)) + .gapWidth(16) + .marginBottom(16) + + List() + .fontFamily(ExampleFonts.montserratRegular, size: 16) + .foregroundStyle(Color(red: 31 / 255, green: 41 / 255, blue: 55 / 255)) + .lineHeight(26) + .bulletColor(Color(red: 107 / 255, green: 114 / 255, blue: 128 / 255)) + .bulletSize(6) + .markerMinWidth(20) + .markerColor(Color(red: 107 / 255, green: 114 / 255, blue: 128 / 255)) + .gapWidth(8) + .marginLeft(24) + .marginBottom(16) + + CodeBlock() + .fontFamily(ExampleFonts.courierPrimeRegular, size: 14) + .foregroundStyle(Color(red: 243 / 255, green: 244 / 255, blue: 246 / 255)) + .backgroundStyle(Color(red: 31 / 255, green: 41 / 255, blue: 55 / 255)) + .borderColor(Color(red: 55 / 255, green: 65 / 255, blue: 81 / 255)) + .borderWidth(1) + .borderRadius(8) + .padding(16) + .lineHeight(22) + .marginBottom(16) + + Code() + .foregroundStyle(Color(red: 124 / 255, green: 58 / 255, blue: 237 / 255)) + .backgroundStyle(Color(red: 245 / 255, green: 243 / 255, blue: 255 / 255)) + + Link() + .fontFamily(ExampleFonts.montserratBold, size: 16) + .foregroundStyle(Color(red: 37 / 255, green: 99 / 255, blue: 235 / 255)) + .underline(true) + + Strong() + .foregroundStyle(Color(red: 17 / 255, green: 24 / 255, blue: 39 / 255)) + + Emphasis() + .foregroundStyle(Color(red: 75 / 255, green: 85 / 255, blue: 99 / 255)) + + BlockImage() + .height(200) + .borderRadius(8) + .marginBottom(16) + + InlineImage() + .size(20) + + ThematicBreak() + .color(Color(red: 229 / 255, green: 231 / 255, blue: 235 / 255)) + .height(1) + .marginTop(24) + .marginBottom(24) +} + +/// Mirrors Android PlaygroundMarkdownStyle. +let PlaygroundMarkdownTheme = MarkdownTheme { + Link() + .foregroundStyle(Color(red: 37 / 255, green: 99 / 255, blue: 235 / 255)) + .underline(true) + + Code() + .foregroundStyle(Color(red: 124 / 255, green: 58 / 255, blue: 237 / 255)) + .backgroundStyle(Color(red: 245 / 255, green: 243 / 255, blue: 255 / 255)) + + CodeBlock() + .foregroundStyle(Color(red: 243 / 255, green: 244 / 255, blue: 246 / 255)) + .backgroundStyle(Color(red: 31 / 255, green: 41 / 255, blue: 55 / 255)) + .borderRadius(8) + + Blockquote() + .foregroundStyle(Color(red: 75 / 255, green: 85 / 255, blue: 99 / 255)) + .borderColor(Color(red: 209 / 255, green: 213 / 255, blue: 219 / 255)) + .borderWidth(3) + .gapWidth(12) +} diff --git a/apps/ios-example/EnrichedMarkdownExample/EnrichedMarkdownExample/ExampleRoute.swift b/apps/ios-example/EnrichedMarkdownExample/EnrichedMarkdownExample/ExampleRoute.swift new file mode 100644 index 000000000..7891968cc --- /dev/null +++ b/apps/ios-example/EnrichedMarkdownExample/EnrichedMarkdownExample/ExampleRoute.swift @@ -0,0 +1,8 @@ +enum ExampleRoute { + case home + case playground + case text + case input + case stream + case storybook +} diff --git a/apps/ios-example/EnrichedMarkdownExample/EnrichedMarkdownExample/HomeScreen.swift b/apps/ios-example/EnrichedMarkdownExample/EnrichedMarkdownExample/HomeScreen.swift new file mode 100644 index 000000000..1cc08f2d8 --- /dev/null +++ b/apps/ios-example/EnrichedMarkdownExample/EnrichedMarkdownExample/HomeScreen.swift @@ -0,0 +1,88 @@ +import SwiftUI + +private struct HomeMenuItem { + let route: ExampleRoute + let label: String + let subtext: String + let color: Color + let accessibilityId: String +} + +private let menuItems: [HomeMenuItem] = [ + HomeMenuItem( + route: .playground, + label: "Playground", + subtext: "live editor with preview", + color: Color(red: 0 / 255, green: 122 / 255, blue: 255 / 255), + accessibilityId: "home-block-playground" + ), + HomeMenuItem( + route: .text, + label: "Text", + subtext: "static markdown rendering", + color: Color(red: 52 / 255, green: 199 / 255, blue: 89 / 255), + accessibilityId: "home-block-text" + ), + HomeMenuItem( + route: .input, + label: "Input", + subtext: "chat-style rich text input", + color: Color(red: 255 / 255, green: 149 / 255, blue: 0 / 255), + accessibilityId: "home-block-input" + ), + HomeMenuItem( + route: .stream, + label: "Stream", + subtext: "streaming markdown with tables", + color: Color(red: 175 / 255, green: 82 / 255, blue: 222 / 255), + accessibilityId: "home-block-stream" + ), + HomeMenuItem( + route: .storybook, + label: "Storybook", + subtext: "component stories", + color: Color(red: 255 / 255, green: 45 / 255, blue: 85 / 255), + accessibilityId: "home-block-storybook" + ), +] + +struct HomeScreen: View { + let onNavigate: (ExampleRoute) -> Void + + var body: some View { + ScrollView { + VStack(spacing: 0) { + Text("Enriched Markdown Examples") + .font(.system(size: 28, weight: .bold)) + .multilineTextAlignment(.center) + .padding(.bottom, 10) + + Text("Explore different markdown rendering and input capabilities") + .font(.system(size: 16)) + .foregroundStyle(Color(red: 102 / 255, green: 102 / 255, blue: 102 / 255)) + .multilineTextAlignment(.center) + .padding(.bottom, 40) + + VStack(spacing: 0) { + ForEach(menuItems.indices, id: \.self) { index in + let item = menuItems[index] + HomeScreenButton( + label: item.label, + subtext: item.subtext, + color: item.color, + accessibilityId: item.accessibilityId + ) { + onNavigate(item.route) + } + } + } + .frame(maxWidth: .infinity) + .padding(.horizontal, 28) + } + .padding(20) + .frame(maxWidth: .infinity) + } + .background(Color(red: 245 / 255, green: 245 / 255, blue: 245 / 255)) + .accessibilityIdentifier("home-screen") + } +} diff --git a/apps/ios-example/EnrichedMarkdownExample/EnrichedMarkdownExample/HomeScreenButton.swift b/apps/ios-example/EnrichedMarkdownExample/EnrichedMarkdownExample/HomeScreenButton.swift new file mode 100644 index 000000000..2b27e194d --- /dev/null +++ b/apps/ios-example/EnrichedMarkdownExample/EnrichedMarkdownExample/HomeScreenButton.swift @@ -0,0 +1,34 @@ +import SwiftUI + +struct HomeScreenButton: View { + let label: String + let subtext: String + let color: Color + let accessibilityId: String + let action: () -> Void + + var body: some View { + Button(action: action) { + VStack(spacing: 2) { + Text(label) + .font(.system(size: 20, weight: .semibold)) + .foregroundStyle(.white) + .frame(maxWidth: .infinity) + .multilineTextAlignment(.center) + + Text(subtext) + .font(.system(size: 12)) + .foregroundStyle(.white.opacity(0.8)) + .frame(maxWidth: .infinity) + .multilineTextAlignment(.center) + } + .padding(.horizontal, 30) + .padding(.vertical, 15) + .frame(maxWidth: .infinity) + .background(color, in: RoundedRectangle(cornerRadius: 10)) + } + .buttonStyle(.plain) + .padding(.vertical, 10) + .accessibilityIdentifier(accessibilityId) + } +} diff --git a/apps/ios-example/EnrichedMarkdownExample/EnrichedMarkdownExample/Info.plist b/apps/ios-example/EnrichedMarkdownExample/EnrichedMarkdownExample/Info.plist index 1dd1cffe8..b05734c96 100644 --- a/apps/ios-example/EnrichedMarkdownExample/EnrichedMarkdownExample/Info.plist +++ b/apps/ios-example/EnrichedMarkdownExample/EnrichedMarkdownExample/Info.plist @@ -5,7 +5,7 @@ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName - EnrichedMarkdownExample + Enriched Markdown Examples CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier diff --git a/apps/ios-example/EnrichedMarkdownExample/EnrichedMarkdownExample/PlaygroundScreen.swift b/apps/ios-example/EnrichedMarkdownExample/EnrichedMarkdownExample/PlaygroundScreen.swift new file mode 100644 index 000000000..9fe2db036 --- /dev/null +++ b/apps/ios-example/EnrichedMarkdownExample/EnrichedMarkdownExample/PlaygroundScreen.swift @@ -0,0 +1,182 @@ +import EnrichedMarkdown +import SwiftUI + +struct PlaygroundScreen: View { + @State private var markdown = "" + @State private var setMarkdownSheetVisible = false + @State private var rawInput = "" + @State private var blockImageURI: String? + @State private var inlineImageURI: String? + + var body: some View { + ScrollView { + VStack(alignment: .leading, spacing: 12) { + HStack(spacing: 8) { + PlaygroundButton(label: "Blur", accessibilityId: "blur-button") {} + PlaygroundButton(label: "Underline", accessibilityId: "underline-button") {} + } + + HStack(spacing: 8) { + PlaygroundButton(label: "Insert Image", accessibilityId: "insert-image-button") { + guard let uri = blockImageURI else { return } + let imageMarkdown = "![logo](\(uri))" + if markdown.isEmpty { + markdown = imageMarkdown + } else { + markdown += "\n\n\(imageMarkdown)" + } + } + PlaygroundButton(label: "Insert Inline Image", accessibilityId: "insert-inline-image-button") { + guard let uri = inlineImageURI else { return } + markdown = "Enriched Markdown is a library for ![icon](\(uri)) React Native." + } + } + + Button { + rawInput = "" + setMarkdownSheetVisible = true + } label: { + Text("Set Raw Markdown") + .font(.system(size: 14, weight: .semibold)) + .foregroundStyle(Color(red: 0 / 255, green: 26 / 255, blue: 114 / 255)) + .frame(maxWidth: .infinity) + .padding(.vertical, 12) + .background( + Color(red: 190 / 255, green: 235 / 255, blue: 208 / 255), + in: RoundedRectangle(cornerRadius: 8) + ) + } + .buttonStyle(.plain) + .accessibilityIdentifier("set-markdown-button") + + Text("Preview") + .font(.system(size: 12, weight: .semibold)) + .foregroundStyle(Color(red: 156 / 255, green: 163 / 255, blue: 175 / 255)) + + Group { + if markdown.isEmpty { + Text("Preview will appear here") + .font(.body.italic()) + .foregroundStyle(Color(red: 156 / 255, green: 163 / 255, blue: 175 / 255)) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(14) + .accessibilityIdentifier("preview-empty") + } else { + EnrichedMarkdownText(markdown) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(14) + .accessibilityIdentifier("preview-text") + } + } + .background(Color.white, in: RoundedRectangle(cornerRadius: 10)) + .overlay( + RoundedRectangle(cornerRadius: 10) + .stroke(Color(red: 209 / 255, green: 213 / 255, blue: 219 / 255), lineWidth: 1) + ) + .accessibilityIdentifier("preview-container") + } + .padding(16) + } + .background(Color(red: 249 / 255, green: 250 / 255, blue: 251 / 255)) + .accessibilityIdentifier("playground-screen") + .markdownTheme(PlaygroundMarkdownTheme) + .onAppear { + blockImageURI = BundleImageLoader.fileURI(named: "logo", extension: "png") + inlineImageURI = BundleImageLoader.fileURI(named: "logo_icon", extension: "png") + } + .sheet(isPresented: $setMarkdownSheetVisible) { + SetMarkdownSheet( + rawInput: $rawInput, + onCancel: { setMarkdownSheetVisible = false }, + onConfirm: { + markdown = rawInput + setMarkdownSheetVisible = false + } + ) + } + } +} + +private struct PlaygroundButton: View { + let label: String + let accessibilityId: String + let action: () -> Void + + var body: some View { + Button(action: action) { + Text(label) + .font(.system(size: 13, weight: .semibold)) + .foregroundStyle(Color(red: 55 / 255, green: 65 / 255, blue: 81 / 255)) + .frame(maxWidth: .infinity) + .padding(.vertical, 12) + .background( + Color(red: 229 / 255, green: 231 / 255, blue: 235 / 255), + in: RoundedRectangle(cornerRadius: 8) + ) + } + .buttonStyle(.plain) + .accessibilityIdentifier(accessibilityId) + } +} + +private struct SetMarkdownSheet: View { + @Binding var rawInput: String + let onCancel: () -> Void + let onConfirm: () -> Void + + var body: some View { + NavigationStack { + VStack(alignment: .leading, spacing: 12) { + Text("Set Raw Markdown") + .font(.system(size: 16, weight: .semibold)) + + TextEditor(text: $rawInput) + .frame(minHeight: 120) + .overlay( + RoundedRectangle(cornerRadius: 8) + .stroke(Color(red: 209 / 255, green: 213 / 255, blue: 219 / 255), lineWidth: 1) + ) + .accessibilityIdentifier("set-markdown-input") + .overlay(alignment: .topLeading) { + if rawInput.isEmpty { + Text("Paste or type markdown...") + .foregroundStyle(Color.secondary) + .padding(.horizontal, 8) + .padding(.vertical, 12) + .allowsHitTesting(false) + } + } + + HStack(spacing: 8) { + Button("Cancel", action: onCancel) + .accessibilityIdentifier("set-markdown-cancel") + + Spacer() + + Button("Set", action: onConfirm) + .fontWeight(.semibold) + .foregroundStyle(Color(red: 0 / 255, green: 26 / 255, blue: 114 / 255)) + .padding(.horizontal, 16) + .padding(.vertical, 8) + .background( + Color(red: 190 / 255, green: 235 / 255, blue: 208 / 255), + in: RoundedRectangle(cornerRadius: 8) + ) + .accessibilityIdentifier("set-markdown-confirm") + } + } + .padding(16) + .navigationBarTitleDisplayMode(.inline) + } + .presentationDetents([.medium]) + } +} + +private enum BundleImageLoader { + static func fileURI(named name: String, extension ext: String) -> String? { + guard let url = Bundle.main.url(forResource: name, withExtension: ext) else { + return nil + } + return url.absoluteString + } +} diff --git a/apps/ios-example/EnrichedMarkdownExample/EnrichedMarkdownExample/Resources/Fonts/CourierPrime-Regular.ttf b/apps/ios-example/EnrichedMarkdownExample/EnrichedMarkdownExample/Resources/Fonts/CourierPrime-Regular.ttf new file mode 100644 index 000000000..4af1ff54c Binary files /dev/null and b/apps/ios-example/EnrichedMarkdownExample/EnrichedMarkdownExample/Resources/Fonts/CourierPrime-Regular.ttf differ diff --git a/apps/ios-example/EnrichedMarkdownExample/EnrichedMarkdownExample/Resources/Fonts/Montserrat-Bold.ttf b/apps/ios-example/EnrichedMarkdownExample/EnrichedMarkdownExample/Resources/Fonts/Montserrat-Bold.ttf new file mode 100644 index 000000000..403358755 Binary files /dev/null and b/apps/ios-example/EnrichedMarkdownExample/EnrichedMarkdownExample/Resources/Fonts/Montserrat-Bold.ttf differ diff --git a/apps/ios-example/EnrichedMarkdownExample/EnrichedMarkdownExample/Resources/Fonts/Montserrat-BoldItalic.ttf b/apps/ios-example/EnrichedMarkdownExample/EnrichedMarkdownExample/Resources/Fonts/Montserrat-BoldItalic.ttf new file mode 100644 index 000000000..db7650d70 Binary files /dev/null and b/apps/ios-example/EnrichedMarkdownExample/EnrichedMarkdownExample/Resources/Fonts/Montserrat-BoldItalic.ttf differ diff --git a/apps/ios-example/EnrichedMarkdownExample/EnrichedMarkdownExample/Resources/Fonts/Montserrat-Italic.ttf b/apps/ios-example/EnrichedMarkdownExample/EnrichedMarkdownExample/Resources/Fonts/Montserrat-Italic.ttf new file mode 100644 index 000000000..5f08df020 Binary files /dev/null and b/apps/ios-example/EnrichedMarkdownExample/EnrichedMarkdownExample/Resources/Fonts/Montserrat-Italic.ttf differ diff --git a/apps/ios-example/EnrichedMarkdownExample/EnrichedMarkdownExample/Resources/Fonts/Montserrat-Medium.ttf b/apps/ios-example/EnrichedMarkdownExample/EnrichedMarkdownExample/Resources/Fonts/Montserrat-Medium.ttf new file mode 100644 index 000000000..c9a39ea16 Binary files /dev/null and b/apps/ios-example/EnrichedMarkdownExample/EnrichedMarkdownExample/Resources/Fonts/Montserrat-Medium.ttf differ diff --git a/apps/ios-example/EnrichedMarkdownExample/EnrichedMarkdownExample/Resources/Fonts/Montserrat-Regular.ttf b/apps/ios-example/EnrichedMarkdownExample/EnrichedMarkdownExample/Resources/Fonts/Montserrat-Regular.ttf new file mode 100644 index 000000000..895e220a1 Binary files /dev/null and b/apps/ios-example/EnrichedMarkdownExample/EnrichedMarkdownExample/Resources/Fonts/Montserrat-Regular.ttf differ diff --git a/apps/ios-example/EnrichedMarkdownExample/EnrichedMarkdownExample/Resources/Fonts/Montserrat-SemiBold.ttf b/apps/ios-example/EnrichedMarkdownExample/EnrichedMarkdownExample/Resources/Fonts/Montserrat-SemiBold.ttf new file mode 100644 index 000000000..161477a81 Binary files /dev/null and b/apps/ios-example/EnrichedMarkdownExample/EnrichedMarkdownExample/Resources/Fonts/Montserrat-SemiBold.ttf differ diff --git a/apps/ios-example/EnrichedMarkdownExample/EnrichedMarkdownExample/Resources/logo.png b/apps/ios-example/EnrichedMarkdownExample/EnrichedMarkdownExample/Resources/logo.png new file mode 100644 index 000000000..f8cb17040 Binary files /dev/null and b/apps/ios-example/EnrichedMarkdownExample/EnrichedMarkdownExample/Resources/logo.png differ diff --git a/apps/ios-example/EnrichedMarkdownExample/EnrichedMarkdownExample/Resources/logo_icon.png b/apps/ios-example/EnrichedMarkdownExample/EnrichedMarkdownExample/Resources/logo_icon.png new file mode 100644 index 000000000..c7ec05598 Binary files /dev/null and b/apps/ios-example/EnrichedMarkdownExample/EnrichedMarkdownExample/Resources/logo_icon.png differ diff --git a/apps/ios-example/EnrichedMarkdownExample/EnrichedMarkdownExample/Resources/sample_markdown.md b/apps/ios-example/EnrichedMarkdownExample/EnrichedMarkdownExample/Resources/sample_markdown.md new file mode 100644 index 000000000..92b45e62e --- /dev/null +++ b/apps/ios-example/EnrichedMarkdownExample/EnrichedMarkdownExample/Resources/sample_markdown.md @@ -0,0 +1,409 @@ + +# The Hidden World of Forest Ecosystems + +Forests cover approximately **31% of the Earth's land surface**, providing habitat for countless species and playing a vital role in our planet's health. These magnificent ecosystems have existed for over *300 million years*, evolving alongside the creatures that call them home. + +Forests cover approximately **31% of the Earth's land surface**, providing habitat for countless species and playing a vital role in our planet's health. These magnificent ecosystems have existed for over *300 million years*, evolving alongside the creatures that call them home. + +![Misty forest at sunrise](https://images.unsplash.com/photo-1448375240586-882707db888b?w=800) + +## Why Forests Matter + +Forests are often called the *lungs of the Earth*. They absorb **carbon dioxide** and release oxygen through photosynthesis — a process essential for all life on our planet. A single mature tree can absorb up to `48 pounds` of CO₂ per year. + +> In every walk with nature, one receives far more than he seeks. `test code` +> +> — John Muir + +### Key Benefits + +- **Climate regulation** through carbon sequestration +- *Biodiversity* hotspots supporting millions of species +- Natural water filtration and ***flood prevention*** +- Source of medicine, food, and raw materials +- Soil erosion prevention and **nutrient cycling** +- Recreation and *mental health* benefits + +### Economic Impact + +Forests contribute over **$1.3 trillion** to the global economy annually. They provide: + +- Timber and *wood products* +- Non-timber forest products like **nuts and berries** +- Ecotourism opportunities +- ***Carbon credits*** for climate mitigation + +--- + +## The Layers of a Forest + +A healthy forest consists of several distinct layers, each supporting unique wildlife: + +- **Emergent Layer** — the tallest trees reaching above the canopy + - Heights exceeding *200 feet* + - Home to **eagles** and other raptors +- **Canopy** — the primary layer receiving direct sunlight + - Home to most *bird species* + - Contains **90% of forest wildlife** + - Dense foliage creating a ***green roof*** +- **Understory** — smaller trees and saplings + - Receives only *2-15%* of sunlight + - **Shade-tolerant** plants thrive here +- **Shrub Layer** — bushes and young trees + - Important for *nesting birds* + - Berry-producing plants +- **Forest Floor** — decomposing matter and ground-dwelling creatures + - Fungi and *microorganisms* + - **Insects** and small mammals + - Nutrient-rich ***humus*** layer + +![Sunlight through forest canopy](https://images.unsplash.com/photo-1542273917363-3b1817f69a2d?w=800) + +--- + +## Types of Forests + +Our planet hosts remarkably diverse forest types, each adapted to its unique climate and conditions. + +### Tropical Rainforests + +Found near the equator, these forests receive **80-400 inches** of rainfall annually. They contain: + +- Over *50%* of all plant and animal species +- The highest **biodiversity** on Earth +- Trees that remain green ***year-round*** +- Multiple canopy layers + +> The rainforest is like a library where all the books are slowly being burned. +> +> — Thomas Lovejoy + +### Temperate Forests + +Located in moderate climate zones, these forests experience **four distinct seasons**: + +- **Deciduous trees** that shed leaves in autumn +- Rich *understory* vegetation +- Diverse wildlife including deer, bears, and ***songbirds*** +- Spectacular fall foliage + +### Boreal Forests (Taiga) + +The largest terrestrial biome, spanning across **Northern Russia, Canada, and Scandinavia**: + +- Dominated by *coniferous* trees +- Short growing seasons of **50-100 days** +- Home to wolves, moose, and ***lynx*** +- Stores more carbon than tropical forests + +![Snow-covered boreal forest](https://images.unsplash.com/photo-1418985991508-e47386d96a71?w=800) + +--- + +## Forest Statistics by Type + +| Forest Type | Coverage | Annual Rainfall | Biodiversity | Carbon Storage | +|------------|----------|-----------------|--------------|----------------| +| Tropical Rainforest | ~7% of land | 80-400 inches | Highest (50%+ species) | High | +| Temperate Forest | ~16% of land | 30-60 inches | Moderate | Moderate | +| Boreal Forest (Taiga) | ~11% of land | 15-40 inches | Lower | Highest | +| Mediterranean Forest | ~2% of land | 20-40 inches | Moderate | Moderate | + +--- + +## Spoiler Examples + +Try tapping the hidden text below to reveal it: + +The secret location of the oldest tree is ||deep in the White Mountains of California||. + +Here is a spoiler with **bold** inside: ||The tree is named **Methuselah** and is over *4,850 years old*||. + +Multiple spoilers in one paragraph: The password is ||swordfish|| and the code is ||42||. + +||The entire Amazon rainforest produces about **20% of the world's oxygen**, but recent studies suggest that the true figure may be closer to *6-9%* because the forest also consumes a significant amount of oxygen through decomposition. Nevertheless, the Amazon remains the single largest tropical rainforest on Earth, spanning **5.5 million square kilometers** across nine countries.|| + +--- + +## Superscript and Subscript Examples + +Mathematical and scientific notation using superscripts and subscripts: + +- Water molecule: H~2~O +- Carbon dioxide: CO~2~ +- Sulfuric acid: H~2~SO~4~ +- Einstein's famous equation: E = mc^2^ +- Pythagorean theorem: a^2^ + b^2^ = c^2^ +- 2^10^ = 1024 + +Footnote-style references^1^ and trademark symbols like React Native^™^ can also use superscripts. + +Scientific prefixes: 6.022 × 10^23^ (Avogadro's number), the universe is approximately 1.38 × 10^10^ years old. + +--- + +## Fascinating Forest Facts + +Did you know that trees communicate through an underground network? Scientists call this the `Wood Wide Web` — a fungal network connecting tree roots across entire forests. + +```javascript +// Example: Tree network simulation +class TreeNetwork { + constructor() { + this.trees = []; + this.fungalConnections = new Map(); + } + + connectTrees(tree1, tree2) { + // Trees share nutrients through mycorrhizal networks + this.fungalConnections.set(`\${tree1.id}-\${tree2.id}`, { + nutrientFlow: 'bidirectional', + signalTransmission: true + }); + } +} +``` + +> Trees share nutrients and information through mycorrhizal networks, essentially "talking" to each other underground. + +### The Science of Tree Communication + +Research has revealed remarkable findings: + +- Mother trees recognize their **offspring** and send them extra nutrients +- Dying trees *dump* their resources into the network +- Trees can ***warn*** neighbors about insect attacks +- Fungi receive `sugars` in exchange for their services + +### Ancient Giants + +Some of the oldest living organisms on Earth are trees: + +- **Methuselah** — a bristlecone pine over *4,850 years old* +- **Hyperion** — the tallest known tree at ***380 feet*** +- **General Sherman** — the largest tree by volume +- **Pando** — a clonal colony spanning *106 acres* +- **Jomon Sugi** — a Japanese cedar over **2,000 years old** + +### Remarkable Adaptations + +Trees have evolved incredible survival strategies: + +- **Redwoods** can absorb fog through their *needles* +- Baobabs store up to ***32,000 gallons*** of water +- Some pines require `fire` to release their seeds +- Mangroves filter *salt* from seawater + +![Mountain landscape with forest](https://images.unsplash.com/photo-1441974231531-c6227db76b6e?w=800) + +--- + +## Forest Wildlife + +Forests support an astonishing array of life, from microscopic organisms to apex predators. + +### Mammals + +- **Large predators** — wolves, bears, mountain lions +- *Herbivores* — deer, elk, moose +- ***Small mammals*** — squirrels, chipmunks, rabbits +- Nocturnal species — bats, owls, foxes + +### Birds + +Over **80% of bird species** depend on forests for: + +- Nesting and *breeding* +- Food sources like ***insects and seeds*** +- Migration stopover points +- Year-round habitat + +### Insects and Invertebrates + +The forest floor alone contains: + +- Thousands of *beetle* species +- **Ants** that aerate soil +- ***Earthworms*** that process organic matter +- Butterflies and moths + +> Look deep into nature, and then you will understand everything better. +> +> — Albert Einstein + +--- + +## Threats to Our Forests + +Despite their importance, forests face unprecedented challenges in the modern era. + +### Deforestation + +Every year, we lose approximately **10 million hectares** of forest — an area the size of *Iceland*. Primary causes include: + +- Agricultural expansion for ***soy and cattle*** +- Logging for timber and `paper products` +- Urban development and *infrastructure* +- Mining operations + +### Climate Change + +Rising temperatures affect forests through: + +- Increased **drought** frequency +- More severe *wildfires* +- ***Pest outbreaks*** spreading to new areas +- Shifting growing seasons + +### Invasive Species + +Non-native species disrupt forest ecosystems: + +- **Emerald ash borer** has killed millions of trees +- *Kudzu* vine smothers native vegetation +- ***Feral pigs*** destroy understory plants + +![Deforested landscape](https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?w=800) + +--- + +## Protecting Our Forests + +Conservation efforts are critical for preserving these vital ecosystems. Here's how you can help: + +### Action Checklist + +- [x] Support *sustainable forestry* practices + - [x] Research certified sustainable wood products + - [ ] Learn about forest management practices +- [x] Reduce paper consumption and **recycle** + - [x] Switch to digital documents when possible + - [ ] Set up paper recycling at home +- [ ] Plant native trees in your community + - [ ] Join local tree planting events + - [ ] Choose appropriate native species +- [ ] Donate to ***conservation organizations*** +- [ ] Choose products with `FSC certification` + - [ ] Look for FSC logo on products + - [ ] Support companies with sustainable practices +- [ ] Advocate for *protected areas* +- [ ] Reduce your **carbon footprint** + - [ ] Use public transportation + - [ ] Reduce energy consumption +- [ ] Educate others about forest importance + +### Priority Actions + +1. ~~Ignore the problem~~ → **Take immediate action** +2. Support *sustainable forestry* practices +3. Reduce paper consumption and **recycle** +4. Plant native trees in your community +5. Donate to ***conservation organizations*** +6. Choose products with `FSC certification` +7. Advocate for *protected areas* +8. Reduce your **carbon footprint** +9. Educate others about forest importance + +> The best time to plant a tree was 20 years ago. The second best time is now. +> +> — Chinese Proverb + +### Success Stories + +Conservation efforts are making a difference: + +- Costa Rica *doubled* its forest cover since **1980** +- China's tree-planting program covers ***500,000 square kilometers*** +- The Amazon Fund has protected millions of hectares +- Community forests in *Nepal* have recovered significantly + +### Organizations Making an Impact + +| Organization | Focus Area | Impact | +|--------------|------------|--------| +| **World Wildlife Fund** | Global conservation | Protected millions of hectares | +| *Rainforest Alliance* | Sustainable agriculture | Certified 5M+ hectares | +| ***The Nature Conservancy*** | Land protection | Protected 125M+ acres | +| One Tree Planted | Reforestation | Planted 100M+ trees | + +--- + +## The Future of Forests + +Scientists and conservationists are developing innovative approaches to protect and restore forests. + +### Technology Solutions + +- **Satellite monitoring** for real-time deforestation alerts +- *Drone planting* that can seed 100,000 trees per day +- ***DNA barcoding*** to track illegal logging +- AI-powered `fire prediction` systems + +```python +# Example: Forest monitoring with satellite data +import satellite_imagery + +def detect_deforestation(region): + """Monitor forest cover changes using satellite imagery""" + current_cover = satellite_imagery.get_forest_cover(region) + previous_cover = satellite_imagery.get_historical_cover(region, years_ago=1) + + deforestation_rate = (previous_cover - current_cover) / previous_cover + if deforestation_rate > 0.05: # 5% threshold + alert_conservation_team(region, deforestation_rate) + + return deforestation_rate +``` + +### Restoration Efforts + +The UN Decade on Ecosystem Restoration aims to restore **350 million hectares** by 2030: + +- Focus on *native species* +- Community-led initiatives +- ***Agroforestry*** integration +- Corridor connections between forests + +![New forest growth](https://images.unsplash.com/photo-1502082553048-f009c37129b9?w=800) + +--- + +## High-Resolution Image Test + +The image below is loaded at full resolution (4000+ pixels wide) to test downsampling. Without downsampling, this single image would consume ~48 MB of memory during decode. With Phase 3 downsampling, it decodes at screen-width resolution instead. + +![High-res forest aerial view](https://images.unsplash.com/photo-1542273917363-3b1817f69a2d?w=4000) + +--- + +## Right-to-Left Text Samples + +Short samples for testing RTL rendering, line measurement, and inline formatting across scripts. + +Arabic: الغابات تغطي حوالي **31%** من سطح اليابسة، وتوفر موطناً لعدد لا يحصى من الأنواع. + +Hebrew: יערות מכסים כ-**31%** משטח היבשה של כדור הארץ ומספקים בית ל*מיליוני* מינים. + +Mixed LTR/RTL in one line: The Hebrew word for "forest" is יַעַר and the Arabic word is غابة — both appear inline with English. + +A longer RTL paragraph to exercise line wrapping and measurement: + +> هذه فقرة طويلة باللغة العربية تهدف إلى اختبار كيفية تعامل المكوّن مع التفاف الأسطر والقياس عندما يكون النص طويلاً بما يكفي ليمتد عبر عدّة أسطر، مع وجود **تنسيق غامق** و*مائل* و[رابط](https://example.com) داخل النص. + +זוהי פסקה ארוכה בעברית שמטרתה לבדוק את אופן הטיפול בשבירת שורות ובמדידה כאשר הטקסט ארוך מספיק כדי להתפרס על פני מספר שורות, עם **טקסט מודגש** ו*נטוי* ו[קישור](https://example.com) בתוך הפסקה. + +--- + +## Conclusion + +Forests are ***irreplaceable ecosystems*** that sustain life on Earth. From the microscopic fungi beneath our feet to the towering giants that touch the sky, every element plays a crucial role. + +By understanding their importance and taking action to protect them, we can ensure these magnificent landscapes thrive for generations to come. Whether you plant a single tree or support global conservation efforts, **every action matters**. + +> The clearest way into the Universe is through a forest wilderness. +> +> — John Muir + +Learn more about forest conservation at [World Wildlife Fund](https://www.worldwildlife.org/habitats/forest-habitat) or [Rainforest Alliance](https://www.rainforest-alliance.org). + +*Together, we can protect the forests that protect us.* diff --git a/apps/ios-example/EnrichedMarkdownExample/EnrichedMarkdownExample/TextScreen.swift b/apps/ios-example/EnrichedMarkdownExample/EnrichedMarkdownExample/TextScreen.swift new file mode 100644 index 000000000..560c16b06 --- /dev/null +++ b/apps/ios-example/EnrichedMarkdownExample/EnrichedMarkdownExample/TextScreen.swift @@ -0,0 +1,20 @@ +import EnrichedMarkdown +import SwiftUI + +struct TextScreen: View { + let markdown: String + + var body: some View { + ScrollView { + EnrichedMarkdownText(markdown) + .markdownTheme(CustomMarkdownTheme) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.horizontal, 16) + .padding(.vertical, 16) + .onLinkPress { url in + UIApplication.shared.open(url) + } + } + .background(Color.white) + } +}