Add share functionality and improve string escaping in FormulaElement.toStringLiteral

- Add share button to formula list with export functionality
- Implement proper escaping of special characters (\n, \t, \", etc.) in FormulaElement.toStringLiteral methods
- Create escapeD4rtString helper function for consistent escaping
- Update Formula, UnitSpec, and VariableSpec toStringLiteral methods to use escaping
- Add share_plus package dependency for sharing functionality

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
Your Name 2026-02-15 12:10:48 +01:00
parent 8b5529dddc
commit 05fd37dd9a
8 changed files with 193 additions and 23 deletions

View file

@ -28,6 +28,6 @@
- [X] If the database is empty, sugest to use a default corpus
- [X] If the user choose to use the default corpus, populate de database with the default corpus (load defaultcorpus, and then use toStringLiteral). If not, start with an empty list of formulas.
- [X] From now on, the corpus will be loaded from database instead of assets
- [R] Create method List<FormulaElement> Corpus.withDependencies(Formula formula). It will return the formula, the units of the formula, and all the units from the corpus with the same base unit.
- [ ] Add a Share button to the formula list. It will export the array string literal of the formula with the units from Corpus.withDependencies().
- [X] Create method List<FormulaElement> Corpus.withDependencies(Formula formula). It will return the formula, the units of the formula, and all the units from the corpus with the same base unit.
- [R] Add a Share button to the formula list. It will export the array string literal of the formula with the units from Corpus.withDependencies().
- [ ] Replace flutter-markdown with flutter-markdown-plus

View file

@ -1,7 +1,9 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; // For Clipboard
import 'package:d4rt_formulas/formula_models.dart';
import '../corpus.dart';
import 'formula_screen.dart';
import 'package:share_plus/share_plus.dart';
class FormulaList extends StatefulWidget {
final Corpus corpus;
@ -41,7 +43,7 @@ class _FormulaListState extends State<FormulaList> {
List<Formula> get _filteredFormulas {
if (_searchQuery.isEmpty) return widget.formulas;
return widget.formulas.where((formula) {
final nameMatch = formula.name.toLowerCase().contains(_searchQuery);
final tagMatch = formula.tags.any((tag) => tag.toLowerCase().contains(_searchQuery));
@ -49,6 +51,73 @@ class _FormulaListState extends State<FormulaList> {
}).toList();
}
void _shareFormula(Formula formula) async {
try {
// Get the formula and its dependencies
final dependencies = widget.corpus.withDependencies(formula);
// Convert each dependency to its string literal representation
final literals = dependencies.map((element) => element.toStringLiteral()).toList();
// Create an array string literal containing all the elements
final exportString = '[${literals.join(', ')}]';
// Share the string
await Share.share(
exportString,
subject: 'Sharing formula: ${formula.name}',
);
} catch (e) {
_showErrorDialog('Error sharing formula: $e');
}
}
void _copyFormula(Formula formula) async {
try {
// Get the formula and its dependencies
final dependencies = widget.corpus.withDependencies(formula);
// Convert each dependency to its string literal representation
final literals = dependencies.map((element) => element.toStringLiteral()).toList();
// Create an array string literal containing all the elements
final exportString = '[${literals.join(', ')}]';
// Copy to clipboard
await Clipboard.setData(ClipboardData(text: exportString));
// Show a snackbar to confirm
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Formula and dependencies copied to clipboard!'),
duration: Duration(seconds: 2),
),
);
} catch (e) {
_showErrorDialog('Error copying formula: $e');
}
}
void _showErrorDialog(String message) {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text('Error'),
content: Text(message),
actions: <Widget>[
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: Text('OK'),
),
],
);
},
);
}
@override
Widget build(BuildContext context) {
return Column(
@ -72,9 +141,41 @@ class _FormulaListState extends State<FormulaList> {
final formula = _filteredFormulas[index];
return ListTile(
title: Text(formula.name),
subtitle: formula.tags.isNotEmpty
subtitle: formula.tags.isNotEmpty
? Text('Tags: ${formula.tags.join(', ')}')
: null,
trailing: PopupMenuButton(
icon: Icon(Icons.share),
onSelected: (value) {
if (value == 'share') {
_shareFormula(formula);
} else if (value == 'copy') {
_copyFormula(formula);
}
},
itemBuilder: (context) => [
PopupMenuItem(
value: 'share',
child: Row(
children: [
Icon(Icons.share),
SizedBox(width: 8),
Text('Share'),
],
),
),
PopupMenuItem(
value: 'copy',
child: Row(
children: [
Icon(Icons.copy),
SizedBox(width: 8),
Text('Copy to clipboard'),
],
),
),
],
),
onTap: () {
Navigator.push(
context,

View file

@ -36,6 +36,16 @@ List<Object?> parseD4rtLiteral(String arrayStringLiteral) {
return list;
}
/// Escapes special characters in a string for use in D4RT literals
String escapeD4rtString(String input) {
return input
.replaceAll(r'\', r'\\') // Escape backslashes first
.replaceAll('\n', r'\n') // Escape newlines
.replaceAll('\r', r'\r') // Escape carriage returns
.replaceAll('\t', r'\t') // Escape tabs
.replaceAll('"', r'\"'); // Escape quotes last
}
/// Parses corpus elements from an array string literal.
/// Determines if each element is a formula or a unit and converts accordingly.
List<FormulaElement> parseCorpusElements(String arrayStringLiteral) {
@ -143,21 +153,21 @@ class UnitSpec implements FormulaElement {
@override
String toStringLiteral() {
final buffer = StringBuffer('{');
buffer.write('"name": "$name", "symbol": "$symbol"');
buffer.write('"name": "${escapeD4rtString(name)}", "symbol": "${escapeD4rtString(symbol)}"');
if (name == baseUnit && factorFromUnitToBase == 1) {
// This is a base unit
buffer.write(', "isBase": true');
} else {
buffer.write(', "baseUnit": "$baseUnit"');
buffer.write(', "baseUnit": "${escapeD4rtString(baseUnit)}"');
if (factorFromUnitToBase != null) {
buffer.write(', "factor": $factorFromUnitToBase');
} else if (codeFromUnitToBase != null && codeFromBaseToUnit != null) {
buffer.write(', "toBase": "$codeFromUnitToBase", "fromBase": "$codeFromBaseToUnit"');
buffer.write(', "toBase": "${escapeD4rtString(codeFromUnitToBase!)}", "fromBase": "${escapeD4rtString(codeFromBaseToUnit!)}"');
}
}
buffer.write('}');
return buffer.toString();
}
@ -200,22 +210,22 @@ class VariableSpec {
@override
String toStringLiteral() {
final buffer = StringBuffer('{');
buffer.write('"name": "$name"');
buffer.write('"name": "${escapeD4rtString(name)}"');
if (unit != null) {
buffer.write(', "unit": "$unit"');
buffer.write(', "unit": "${escapeD4rtString(unit!)}"');
}
if (values != null && values!.isNotEmpty) {
buffer.write(', "values": [${values!.map((value) {
if (value is String) {
return '"$value"';
return '"${escapeD4rtString(value)}"';
} else {
return value.toString();
}
}).join(", ")}]');
}
buffer.write('}');
return buffer.toString();
}
@ -337,22 +347,33 @@ class Formula implements FormulaElement {
/// by the D4RT parser to recreate the same Formula object.
String toStringLiteral() {
final inputStrings = input.map((varSpec) => varSpec.toStringLiteral()).toList();
final buffer = StringBuffer('{');
buffer.write('"name": "$name"');
if (description != null) {
buffer.write(', "description": "$description"');
buffer.write(', "description": "${escapeD4rtString(description!)}"');
}
buffer.write(', "input": [${inputStrings.join(", ")}]');
buffer.write(', "output": ${output.toStringLiteral()}');
buffer.write(', "d4rtCode": ${d4rtCode.contains('\n') || d4rtCode.contains('"') ? 'r"""$d4rtCode"""' : '"$d4rtCode"'}');
if (tags.isNotEmpty) {
buffer.write(', "tags": [${tags.map((tag) => '"$tag"').join(", ")}]');
// Handle d4rtCode with proper escaping
String escapedD4rtCode;
if (d4rtCode.contains('\n') || d4rtCode.contains('"')) {
// For multiline strings or strings with quotes, use raw string but still escape internal quotes
escapedD4rtCode = 'r"""${d4rtCode.replaceAll('"', '\\"')}"""';
} else {
// For single-line strings, use escaped version
escapedD4rtCode = '"${escapeD4rtString(d4rtCode)}"';
}
buffer.write(', "d4rtCode": $escapedD4rtCode');
if (tags.isNotEmpty) {
buffer.write(', "tags": [${tags.map((tag) => '"${escapeD4rtString(tag)}"').join(", ")}]');
}
buffer.write('}');
return buffer.toString();
}

View file

@ -5,10 +5,12 @@
import FlutterMacOS
import Foundation
import share_plus
import sqlite3_flutter_libs
import url_launcher_macos
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
Sqlite3FlutterLibsPlugin.register(with: registry.registrar(forPlugin: "Sqlite3FlutterLibsPlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
}

View file

@ -201,6 +201,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.15.0"
cross_file:
dependency: transitive
description:
name: cross_file
sha256: "28bb3ae56f117b5aec029d702a90f57d285cd975c3c5c281eaca38dbc47c5937"
url: "https://pub.dev"
source: hosted
version: "0.3.5+2"
crypto:
dependency: transitive
description:
@ -752,6 +760,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.3.8"
share_plus:
dependency: "direct main"
description:
name: share_plus
sha256: fce43200aa03ea87b91ce4c3ac79f0cecd52e2a7a56c7a4185023c271fbfa6da
url: "https://pub.dev"
source: hosted
version: "10.1.4"
share_plus_platform_interface:
dependency: transitive
description:
name: share_plus_platform_interface
sha256: cc012a23fc2d479854e6c80150696c4a5f5bb62cb89af4de1c505cf78d0a5d0b
url: "https://pub.dev"
source: hosted
version: "5.0.2"
shelf:
dependency: transitive
description:
@ -997,6 +1021,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.1.5"
uuid:
dependency: transitive
description:
name: uuid
sha256: a11b666489b1954e01d992f3d601b1804a33937b5a8fe677bd26b8a9f96f96e8
url: "https://pub.dev"
source: hosted
version: "4.5.2"
vector_graphics:
dependency: transitive
description:
@ -1077,6 +1109,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.2.1"
win32:
dependency: transitive
description:
name: win32
sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e
url: "https://pub.dev"
source: hosted
version: "5.15.0"
xdg_directories:
dependency: "direct main"
description:

View file

@ -51,6 +51,8 @@ dependencies:
ffi: ^2.0.1
collection: any
share_plus: ^10.0.3
dev_dependencies:
flutter_test:
sdk: flutter

View file

@ -6,10 +6,13 @@
#include "generated_plugin_registrant.h"
#include <share_plus/share_plus_windows_plugin_c_api.h>
#include <sqlite3_flutter_libs/sqlite3_flutter_libs_plugin.h>
#include <url_launcher_windows/url_launcher_windows.h>
void RegisterPlugins(flutter::PluginRegistry* registry) {
SharePlusWindowsPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi"));
Sqlite3FlutterLibsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("Sqlite3FlutterLibsPlugin"));
UrlLauncherWindowsRegisterWithRegistrar(

View file

@ -3,6 +3,7 @@
#
list(APPEND FLUTTER_PLUGIN_LIST
share_plus
sqlite3_flutter_libs
url_launcher_windows
)