Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@
library;

import 'dart:async';
import 'dart:io';

import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:flutter/material.dart';

import 'package:markdown_widget_builder/markdown_widget_builder.dart';
Expand Down Expand Up @@ -65,7 +65,7 @@ class _MarkdownExamplePageState extends State<MarkdownExamplePage> {
bool _isLoadingConfig = true;
Object? _configLoadError;

StreamSubscription<FileSystemEvent>? _fileWatchSub;
StreamSubscription<dynamic>? _fileWatchSub;
String _markdownContent = 'Loading...';

@override
Expand Down Expand Up @@ -103,11 +103,11 @@ class _MarkdownExamplePageState extends State<MarkdownExamplePage> {
);
setState(() => _markdownContent = content);

// Watch file changes (if local path is valid).
// Watch file changes (if local path is valid and not on web).
// watchFileChanges returns null on web or if file watching is unavailable.

final interpretedPath = await interpretPath(config.markdown.path);
final file = File(interpretedPath);
if (await file.exists()) {
if (!kIsWeb) {
final interpretedPath = await interpretPath(config.markdown.path);
_fileWatchSub?.cancel();
_fileWatchSub = watchFileChanges(
interpretedPath,
Expand Down
87 changes: 68 additions & 19 deletions lib/src/utils/file_ops.dart
Original file line number Diff line number Diff line change
Expand Up @@ -32,18 +32,19 @@ library;

import 'dart:async';
import 'dart:convert';
import 'dart:io';

import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:flutter/services.dart' show rootBundle;

import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart'
show getApplicationDocumentsDirectory;

import 'package:markdown_widget_builder/markdown_widget_builder.dart'
show setMarkdownMediaPath;
import 'package:markdown_widget_builder/src/constants/pkg.dart'
show defaultConfigFile, mdPath, mediaPath;
import 'package:markdown_widget_builder/src/utils/platform_io.dart'
if (dart.library.html) 'package:markdown_widget_builder/src/utils/platform_web.dart'
as platform_utils;

/// Structure of md_config.json.

Expand Down Expand Up @@ -103,8 +104,14 @@ class Config {
/// platforms.

Future<String> getAppDirectory() async {
final os = Platform.operatingSystem;
final exePath = Platform.resolvedExecutable;
if (kIsWeb) {
// On web, return a fallback path as there is no file system access.

return '';
}

final os = platform_utils.getOperatingSystem();
final exePath = platform_utils.getResolvedExecutable();

if (os == 'macos') {
final exeDir = p.dirname(exePath);
Expand All @@ -115,8 +122,8 @@ Future<String> getAppDirectory() async {
} else if (os == 'windows' || os == 'linux') {
return p.dirname(exePath);
} else if (os == 'android' || os == 'ios') {
final docDir = await getApplicationDocumentsDirectory();
return docDir.path;
final docDirPath = await platform_utils.getApplicationDocumentsPath();
return docDirPath;
} else {
return p.dirname(exePath);
}
Expand Down Expand Up @@ -172,6 +179,13 @@ Future<void> loadMediaFiles(
String rawMediaPath, {
Function(String)? onError,
}) async {
if (kIsWeb) {
// On web, always use asset path as there is no local file system access.

setMarkdownMediaPath(mediaPath);
return;
}

if (rawMediaPath.trim().isEmpty) {
// If the path is empty, fallback to assets.

Expand All @@ -184,9 +198,9 @@ Future<void> loadMediaFiles(
}

final interpretedMediaPath = await interpretPath(rawMediaPath);
Directory dir = Directory(interpretedMediaPath);
final dirExists = await platform_utils.directoryExists(interpretedMediaPath);

if (!await dir.exists()) {
if (!dirExists) {
// If the media directory does not exist, fallback to the default.

onError?.call('Media directory not found: $interpretedMediaPath. '
Expand All @@ -205,6 +219,24 @@ Future<String> loadMarkdownContent(
String rawPath, {
Function(String)? onError,
}) async {
if (kIsWeb) {
// On web, always load from assets.

if (rawPath.trim().isEmpty) {
return rootBundle.loadString(mdPath);
}
try {
return await rootBundle.loadString(rawPath);
} catch (e) {
try {
return await rootBundle.loadString(mdPath);
} catch (e) {
onError?.call('Error loading asset: $e');
return 'Error: Could not load asset.';
}
}
}

if (rawPath.trim().isEmpty) {
// If the path is empty, fallback to assets.

Expand All @@ -214,9 +246,9 @@ Future<String> loadMarkdownContent(
return rootBundle.loadString(mdPath);
}
final interpretedPath = await interpretPath(rawPath);
File file = File(interpretedPath);
final fileExists = await platform_utils.fileExists(interpretedPath);

if (!await file.exists()) {
if (!fileExists) {
// If the file does not exist, fallback to assets.

onError?.call('Markdown file not found at $interpretedPath. '
Expand All @@ -228,23 +260,40 @@ Future<String> loadMarkdownContent(
return 'Error: Could not load fallback asset.';
}
} else {
return file.readAsString();
return platform_utils.readFileAsString(interpretedPath);
}
}

/// File watcher. Returns a subscription object that the caller can cancel at
/// an appropriate time.
/// Returns null on web platform as file watching is not supported.

StreamSubscription<FileSystemEvent> watchFileChanges(
StreamSubscription<dynamic>? watchFileChanges(
String filePath, {
required void Function(String newContent) onFileContentChanged,
}) {
final parentDir = Directory(p.dirname(filePath));
return parentDir.watch().listen((event) async {
if (event.type == FileSystemEvent.modify && event.path == filePath) {
final f = File(filePath);
if (await f.exists()) {
final updated = await f.readAsString();
if (kIsWeb) {
// File watching is not supported on web.

return null;
}

final parentDirPath = p.dirname(filePath);
final watchStream = platform_utils.watchDirectory(parentDirPath);

if (watchStream == null) {
return null;
}

return watchStream.listen((event) async {
// On non-web platforms, event is FileSystemEvent.

if (event.type == 2 && event.path == filePath) {
// type 2 is FileSystemEvent.modify

final fileExists = await platform_utils.fileExists(filePath);
if (fileExists) {
final updated = await platform_utils.readFileAsString(filePath);
onFileContentChanged(updated);
}
}
Expand Down
107 changes: 107 additions & 0 deletions lib/src/utils/platform_io.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/// Platform utilities for non-web platforms (using dart:io).
///
/// Copyright (C) 2026, Software Innovation Institute, ANU.
///
/// Licensed under the MIT License (the "License").
///
/// License: https://choosealicense.com/licenses/mit/.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
///
/// Authors: Tony Chen

library;

import 'dart:io';
import 'dart:typed_data';

import 'package:path_provider/path_provider.dart'
show getTemporaryDirectory, getApplicationDocumentsDirectory;

/// Checks if a file exists at the given path.
/// Returns false on web platform.

Future<bool> fileExists(String path) async {
final file = File(path);
return file.exists();
}

/// Writes bytes to a temporary file and returns the path.
/// Throws on web platform.

Future<String> writeBytesToTempFile(String filename, Uint8List bytes) async {
final tempDir = await getTemporaryDirectory();
final fileNameOnly = filename.split('/').last;
final tempPath = '${tempDir.path}/$fileNameOnly';
final tempFile = File(tempPath);
await tempFile.writeAsBytes(bytes);
return tempFile.path;
}

/// Returns the operating system name.
/// Returns 'web' on web platform.

String getOperatingSystem() {
return Platform.operatingSystem;
}

/// Returns the resolved executable path.
/// Returns empty string on web platform.

String getResolvedExecutable() {
return Platform.resolvedExecutable;
}

/// Returns the application documents directory path.
/// Throws on web platform.

Future<String> getApplicationDocumentsPath() async {
final docDir = await getApplicationDocumentsDirectory();
return docDir.path;
}

/// Checks if a directory exists at the given path.
/// Returns false on web platform.

Future<bool> directoryExists(String path) async {
final dir = Directory(path);
return dir.exists();
}

/// Reads a file as a string.
/// Throws on web platform.

Future<String> readFileAsString(String path) async {
final file = File(path);
return file.readAsString();
}

/// Creates a Directory object for watching file changes.
/// Returns null on web platform.

Stream<FileSystemEvent>? watchDirectory(String path) {
return Directory(path).watch();
}

/// Creates a File object from a path.
/// Throws on web platform.

File createFile(String path) {
return File(path);
}
Loading